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 profile = contact.contactProfile;
|
||||||
const hasSavedContact = Boolean(profile?.created_at || profile?.name || profile?.company || profile?.note);
|
const hasSavedContact = Boolean(profile?.created_at || profile?.name || profile?.company || profile?.note);
|
||||||
if (!hasSavedContact) return null;
|
if (!hasSavedContact) return null;
|
||||||
@ -118,12 +118,23 @@ function SavedContactLabel({ contact }) {
|
|||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
title="Contato salvo na agenda"
|
title="Contato salvo na agenda"
|
||||||
|
aria-label="Contato salvo na agenda"
|
||||||
style={{
|
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',
|
color: '#b7791f',
|
||||||
flex: '0 0 auto',
|
flex: '0 0 auto',
|
||||||
fontSize: '0.72rem',
|
display: 'inline-grid',
|
||||||
fontWeight: 800,
|
placeItems: 'center',
|
||||||
lineHeight: 1,
|
fontSize: 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
•Salvo•
|
•Salvo•
|
||||||
@ -213,9 +224,9 @@ export function ChatConversationList({
|
|||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', gap: '0.75rem' }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', gap: '0.75rem' }}>
|
||||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: '0.45rem', minWidth: 0 }}>
|
<span style={{ display: 'inline-flex', alignItems: 'center', gap: '0.45rem', minWidth: 0 }}>
|
||||||
|
<SavedContactIcon contact={contact} />
|
||||||
<ChannelBadge channel={contact.channel} />
|
<ChannelBadge channel={contact.channel} />
|
||||||
<SpecialtyBadge contact={contact} />
|
<SpecialtyBadge contact={contact} />
|
||||||
<SavedContactLabel contact={contact} />
|
|
||||||
</span>
|
</span>
|
||||||
<UnreadBadge count={contact.unread} />
|
<UnreadBadge count={contact.unread} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useMemo, useRef } from 'react';
|
import { Fragment, useEffect, useMemo, useRef } from 'react';
|
||||||
|
|
||||||
function getMediaUrl(media) {
|
function getMediaUrl(media) {
|
||||||
if (!media?.data || !media?.mimetype) return '';
|
if (!media?.data || !media?.mimetype) return '';
|
||||||
@ -29,6 +29,68 @@ function formatMessageTime(timestamp) {
|
|||||||
return date.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' });
|
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 }) {
|
function MediaRenderer({ message, contactId, onLoadMedia, isAgent }) {
|
||||||
const mediaUrl = useMemo(() => getMediaUrl(message.media), [message.media]);
|
const mediaUrl = useMemo(() => getMediaUrl(message.media), [message.media]);
|
||||||
const mimetype = message.media?.mimetype || '';
|
const mimetype = message.media?.mimetype || '';
|
||||||
@ -414,16 +476,21 @@ 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))',
|
'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 isAgent = message.sender === 'agent';
|
||||||
const isSystem = message.sender === 'system';
|
const isSystem = message.sender === 'system';
|
||||||
const parsedText = parseMessageText(message.text);
|
const parsedText = parseMessageText(message.text);
|
||||||
const messageTime = formatMessageTime(message.timestamp);
|
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) {
|
if (isSystem) {
|
||||||
return (
|
return (
|
||||||
|
<Fragment key={message.id}>
|
||||||
|
{shouldShowDateSeparator ? <DateSeparator label={dateSeparator} /> : null}
|
||||||
<div
|
<div
|
||||||
key={message.id}
|
|
||||||
style={{
|
style={{
|
||||||
justifySelf: 'center',
|
justifySelf: 'center',
|
||||||
padding: '0.7rem 1rem',
|
padding: '0.7rem 1rem',
|
||||||
@ -436,12 +503,14 @@ export function ChatWindow({
|
|||||||
>
|
>
|
||||||
{message.text}
|
{message.text}
|
||||||
</div>
|
</div>
|
||||||
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Fragment key={message.id}>
|
||||||
|
{shouldShowDateSeparator ? <DateSeparator label={dateSeparator} /> : null}
|
||||||
<div
|
<div
|
||||||
key={message.id}
|
|
||||||
style={{
|
style={{
|
||||||
justifySelf: isAgent ? 'end' : 'start',
|
justifySelf: isAgent ? 'end' : 'start',
|
||||||
maxWidth: isMobile ? '88%' : '72%',
|
maxWidth: isMobile ? '88%' : '72%',
|
||||||
@ -499,6 +568,7 @@ export function ChatWindow({
|
|||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
</Fragment>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
@ -606,7 +676,7 @@ export function ChatWindow({
|
|||||||
onChange={(event) => setDraft(event.target.value)}
|
onChange={(event) => setDraft(event.target.value)}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
if (event.key === 'Enter') {
|
if (event.key === 'Enter') {
|
||||||
onSend();
|
onSend?.(draft);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={!safeContact.id || !canReply}
|
disabled={!safeContact.id || !canReply}
|
||||||
@ -633,7 +703,7 @@ export function ChatWindow({
|
|||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onSend}
|
onClick={() => onSend?.(draft)}
|
||||||
disabled={!safeContact.id || !canReply}
|
disabled={!safeContact.id || !canReply}
|
||||||
style={{
|
style={{
|
||||||
border: 'none',
|
border: 'none',
|
||||||
|
|||||||
@ -3,14 +3,9 @@ import { useWhatsappSocket } from '../../../shared/hooks/useWhatsappSocket';
|
|||||||
import { API_BASE_URL } from '../../../shared/services/apiConfig';
|
import { API_BASE_URL } from '../../../shared/services/apiConfig';
|
||||||
import { getAccessOptions, getAccessUsers } from '../../management/services/adminAccessService';
|
import { getAccessOptions, getAccessUsers } from '../../management/services/adminAccessService';
|
||||||
import { getCurrentUser, getCurrentUserProfile } from '../../auth/services/sessionService';
|
import { getCurrentUser, getCurrentUserProfile } from '../../auth/services/sessionService';
|
||||||
import { chatContacts, transferAreas as fallbackTransferAreas } from '../services/chatMocks';
|
import { transferAreas as fallbackTransferAreas } from '../services/chatMocks';
|
||||||
|
|
||||||
function buildInitialMessages() {
|
const MAX_ATTACHMENT_SIZE_BYTES = 15 * 1024 * 1024;
|
||||||
return chatContacts.reduce((acc, contact) => {
|
|
||||||
acc[contact.id] = contact.messages;
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLastMessageFromMe(messages = []) {
|
function getLastMessageFromMe(messages = []) {
|
||||||
const lastMessage = [...messages].reverse().find(isDisplayableMessage);
|
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) {
|
function normalizeComparableContact(contact) {
|
||||||
return {
|
return {
|
||||||
id: contact.id,
|
id: contact.id,
|
||||||
@ -217,14 +203,14 @@ export function useChat() {
|
|||||||
const currentUserAreas = getUserAreas(currentUser);
|
const currentUserAreas = getUserAreas(currentUser);
|
||||||
const isAdminUser = currentUserProfile === 'admin';
|
const isAdminUser = currentUserProfile === 'admin';
|
||||||
const { status: whatsappStatus, incomingMessage, clearIncomingMessage } = useWhatsappSocket();
|
const { status: whatsappStatus, incomingMessage, clearIncomingMessage } = useWhatsappSocket();
|
||||||
const [contacts, setContacts] = useState(buildFallbackContacts);
|
const [contacts, setContacts] = useState([]);
|
||||||
const [activeContactId, setActiveContactId] = useState(chatContacts[0].id);
|
const [activeContactId, setActiveContactId] = useState('');
|
||||||
const [messagesByContact, setMessagesByContact] = useState(buildInitialMessages);
|
const [messagesByContact, setMessagesByContact] = useState({});
|
||||||
const [draft, setDraft] = useState('');
|
const [draft, setDraft] = useState('');
|
||||||
const [attachedFile, setAttachedFile] = useState(null);
|
const [attachedFile, setAttachedFile] = useState(null);
|
||||||
const [areaOptions, setAreaOptions] = useState([]);
|
const [areaOptions, setAreaOptions] = useState([]);
|
||||||
const [accessUsers, setAccessUsers] = useState([]);
|
const [accessUsers, setAccessUsers] = useState([]);
|
||||||
const [selectedArea, setSelectedArea] = useState(chatContacts[0].area);
|
const [selectedArea, setSelectedArea] = useState('Sem fila');
|
||||||
const [isTransferOpen, setIsTransferOpen] = useState(false);
|
const [isTransferOpen, setIsTransferOpen] = useState(false);
|
||||||
const [transferArea, setTransferArea] = useState(currentUserAreas[0] || 'Suporte');
|
const [transferArea, setTransferArea] = useState(currentUserAreas[0] || 'Suporte');
|
||||||
const [transferAttendant, setTransferAttendant] = useState('');
|
const [transferAttendant, setTransferAttendant] = useState('');
|
||||||
@ -329,11 +315,9 @@ export function useChat() {
|
|||||||
|
|
||||||
async function loadChats({ showLoading = false } = {}) {
|
async function loadChats({ showLoading = false } = {}) {
|
||||||
if (whatsappStatus !== 'CONNECTED') {
|
if (whatsappStatus !== 'CONNECTED') {
|
||||||
const fallbackContacts = buildFallbackContacts();
|
setContacts((current) => (current.length ? [] : current));
|
||||||
setContacts((current) => (areContactListsEqual(current, fallbackContacts) ? current : fallbackContacts));
|
setActiveContactId('');
|
||||||
setActiveContactId((current) =>
|
setMessagesByContact({});
|
||||||
fallbackContacts.some((contact) => contact.id === current) ? current : fallbackContacts[0]?.id,
|
|
||||||
);
|
|
||||||
setIsLoadingChats(false);
|
setIsLoadingChats(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -502,12 +486,18 @@ export function useChat() {
|
|||||||
|
|
||||||
async function attachFile(file) {
|
async function attachFile(file) {
|
||||||
if (!file) return;
|
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);
|
const data = await fileToBase64(file);
|
||||||
setAttachedFile({
|
setAttachedFile({
|
||||||
name: file.name,
|
name: file.name,
|
||||||
type: file.type || 'application/octet-stream',
|
type: file.type || 'application/octet-stream',
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
|
setApiError(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeAttachedFile() {
|
function removeAttachedFile() {
|
||||||
@ -597,7 +587,8 @@ export function useChat() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function sendMessage(messageText = draft, contactId = activeContactId) {
|
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;
|
if (!trimmed && !attachedFile) return;
|
||||||
|
|
||||||
const targetContact = contacts.find((contact) => contact.id === contactId) || activeContact;
|
const targetContact = contacts.find((contact) => contact.id === contactId) || activeContact;
|
||||||
@ -650,7 +641,7 @@ export function useChat() {
|
|||||||
if (!contactId.includes('@')) return;
|
if (!contactId.includes('@')) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fetch(`${API_BASE_URL}/whatsapp/send`, {
|
const response = await fetch(`${API_BASE_URL}/whatsapp/send`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@ -660,6 +651,15 @@ export function useChat() {
|
|||||||
media,
|
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);
|
setApiError(null);
|
||||||
updateContact(contactId, (contact) => ({
|
updateContact(contactId, (contact) => ({
|
||||||
...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 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 = [
|
export const quickReplies = [
|
||||||
'Recebi sua mensagem e já vou verificar.',
|
'Recebi sua mensagem e ja vou verificar.',
|
||||||
'Consegue me confirmar o número do protocolo?',
|
'Consegue me confirmar o numero do protocolo?',
|
||||||
'Posso seguir com essa atualização por aqui.',
|
'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 { useNavigate } from 'react-router-dom';
|
||||||
import { createAgentNote, deleteAgentNote, listAgentNotes } from '../services/agentNotesService';
|
import { createAgentNote, deleteAgentNote, listAgentNotes } from '../services/agentNotesService';
|
||||||
import { getCurrentUser } from '../../auth/services/sessionService';
|
import { getCurrentUser } from '../../auth/services/sessionService';
|
||||||
@ -123,6 +123,68 @@ function formatMessageTime(timestamp) {
|
|||||||
return date.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' });
|
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) {
|
function getUserId(user) {
|
||||||
const value = user?.databaseId || user?.id;
|
const value = user?.databaseId || user?.id;
|
||||||
const numeric = Number(value);
|
const numeric = Number(value);
|
||||||
@ -408,14 +470,20 @@ export function MessagesWorkspace({
|
|||||||
'linear-gradient(180deg, rgba(245, 248, 251, 0.45), rgba(255, 255, 255, 0.9))',
|
'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 isAgent = message.from === 'agent';
|
||||||
const parsedText = parseMessageText(message.text);
|
const parsedText = parseMessageText(message.text);
|
||||||
const messageTime = formatMessageTime(message.timestamp);
|
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 (
|
return (
|
||||||
|
<Fragment key={message.id}>
|
||||||
|
{shouldShowDateSeparator ? <DateSeparator label={dateSeparator} /> : null}
|
||||||
<div
|
<div
|
||||||
key={message.id}
|
|
||||||
style={{
|
style={{
|
||||||
justifySelf: isAgent ? 'end' : 'start',
|
justifySelf: isAgent ? 'end' : 'start',
|
||||||
maxWidth: '72%',
|
maxWidth: '72%',
|
||||||
@ -464,6 +532,7 @@ export function MessagesWorkspace({
|
|||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
</Fragment>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user