@adatechnology/conversations-ui 0.1.0-rc.4 → 0.1.0-rc.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ConversationSimulatorPanel--5fIzXWY.d.ts +804 -0
- package/dist/chunk-BJNRLLDO.js +2708 -0
- package/dist/chunk-DKPXKQGC.js +110 -0
- package/dist/{chunk-OGRRHQQW.js → chunk-WCBDXZ3X.js} +68 -4
- package/dist/flows/index.d.ts +422 -5
- package/dist/flows/index.js +2389 -678
- package/dist/index.d.ts +1171 -42
- package/dist/index.js +3755 -843
- package/dist/preview/index.d.ts +157 -42
- package/dist/preview/index.js +772 -191
- package/dist/styles.css +893 -0
- package/package.json +9 -8
- package/src/AudioPlayer.tsx +8 -0
- package/src/AudioRecorderButton.test.tsx +30 -0
- package/src/AudioRecorderButton.tsx +248 -0
- package/src/AudioTranscription.test.tsx +115 -0
- package/src/AudioTranscription.tsx +252 -0
- package/src/Avatar.tsx +14 -3
- package/src/ConversationContextPanel.tsx +218 -44
- package/src/ConversationDocumentsPanel.tsx +347 -24
- package/src/ConversationHeader.test.tsx +66 -0
- package/src/ConversationHeader.tsx +163 -45
- package/src/ConversationListItem.tsx +19 -2
- package/src/ConversationLocalesProvider.tsx +28 -0
- package/src/ConversationRow.tsx +31 -7
- package/src/DarkModeToggle.test.tsx +76 -0
- package/src/DarkModeToggle.tsx +92 -0
- package/src/DocumentsLibrary.tsx +382 -0
- package/src/EmojiPicker.tsx +70 -55
- package/src/FileIcon.test.ts +83 -0
- package/src/FileIcon.tsx +88 -11
- package/src/InteractiveMessage.test.tsx +41 -0
- package/src/InteractiveMessage.tsx +146 -0
- package/src/Lightbox.tsx +18 -3
- package/src/MediaRenderer.tsx +92 -16
- package/src/MessageBubble.test.tsx +41 -0
- package/src/MessageBubble.tsx +75 -6
- package/src/MessageComposer.test.tsx +35 -0
- package/src/MessageComposer.tsx +155 -19
- package/src/RichMessageComposer.test.tsx +113 -0
- package/src/RichMessageComposer.tsx +551 -0
- package/src/SimpleEmojiPicker.tsx +5 -3
- package/src/StatusTicks.tsx +1 -1
- package/src/Toast.tsx +4 -0
- package/src/Tooltip.test.ts +42 -0
- package/src/Tooltip.tsx +167 -0
- package/src/Wallpaper.test.tsx +21 -0
- package/src/Wallpaper.tsx +67 -7
- package/src/WhatsAppMessageEditor.tsx +34 -7
- package/src/WindowExpiredNotice.tsx +12 -4
- package/src/audioRecorderFormat.test.ts +67 -0
- package/src/buildOutput.test.ts +79 -0
- package/src/composer.constant.ts +33 -0
- package/src/conversationTranscript.test.ts +57 -0
- package/src/conversationTranscript.ts +29 -4
- package/src/conversationWindow.ts +7 -5
- package/src/documentTypeLabel.test.ts +57 -0
- package/src/documents/DocumentsWorkspace.tsx +550 -0
- package/src/documents/index.ts +8 -0
- package/src/documents/labels.ts +92 -0
- package/src/emojiCatalog.test.ts +35 -0
- package/src/emojiCatalog.ts +189 -0
- package/src/flows/FlowConnectionEdge.tsx +104 -0
- package/src/flows/FlowGroupHeader.tsx +12 -2
- package/src/flows/FlowLegend.tsx +125 -0
- package/src/flows/FlowMapCanvas.tsx +15 -12
- package/src/flows/FlowMapNode.tsx +4 -1
- package/src/flows/FlowNodeCard.tsx +219 -34
- package/src/flows/FlowNodePanel.tsx +153 -39
- package/src/flows/FlowPalette.tsx +106 -69
- package/src/flows/FlowPortalNode.tsx +1 -1
- package/src/flows/FlowWhatsAppPreview.tsx +14 -3
- package/src/flows/FlowsWorkspace.tsx +1219 -0
- package/src/flows/flowCanvasModel.test.ts +456 -0
- package/src/flows/flowCanvasModel.ts +378 -0
- package/src/flows/flowEditorOps.test.ts +276 -0
- package/src/flows/flowEditorOps.ts +202 -0
- package/src/flows/flowGraph.ts +78 -53
- package/src/flows/index.ts +51 -2
- package/src/flows/labels.ts +180 -0
- package/src/flows/workspaceContract.test.ts +126 -0
- package/src/hooks/useContainerWidth.ts +35 -0
- package/src/hooks/useConversationActions.ts +56 -0
- package/src/hooks/useConversationDocuments.ts +11 -7
- package/src/hooks/useConversationList.ts +15 -9
- package/src/hooks/useConversationMessages.ts +2 -2
- package/src/hooks/useConversationRealtime.ts +10 -8
- package/src/hooks/useScrollToLatestMessage.ts +127 -0
- package/src/hooks/useUrlFilterState.ts +107 -0
- package/src/icon.constant.ts +12 -0
- package/src/index.ts +129 -13
- package/src/lib/cn.test.ts +29 -0
- package/src/lib/composer-formatting.test.ts +78 -0
- package/src/lib/composer-formatting.ts +145 -0
- package/src/lib/createMediaUrlResolver.ts +33 -0
- package/src/lib/paginated.test.ts +33 -0
- package/src/lib/paginated.ts +26 -0
- package/src/lib/whatsapp-formatting.test.tsx +37 -0
- package/src/lib/whatsapp-formatting.tsx +28 -3
- package/src/listing/index.tsx +202 -0
- package/src/pagination.constant.ts +10 -0
- package/src/preview/ConversationPreview.tsx +225 -17
- package/src/preview/ConversationSimulatorClient.ts +143 -0
- package/src/preview/ConversationSimulatorPanel.test.tsx +55 -0
- package/src/preview/ConversationSimulatorPanel.tsx +131 -0
- package/src/preview/MediaTypesPreview.tsx +87 -0
- package/src/preview/conversationPreviewFailures.test.ts +64 -0
- package/src/preview/createMockConversationsApi.ts +175 -15
- package/src/preview/createPreviewBridgeClient.test.ts +92 -0
- package/src/preview/createPreviewBridgeClient.ts +124 -0
- package/src/preview/createPreviewMediaUploader.ts +82 -0
- package/src/preview/createPreviewWebhookClient.test.ts +96 -0
- package/src/preview/createPreviewWebhookClient.ts +127 -4
- package/src/preview/index.ts +51 -3
- package/src/preview/mediaTypeOf.test.ts +15 -0
- package/src/preview/mockDocumentsSearch.test.ts +57 -0
- package/src/preview/preview.test.ts +5 -3
- package/src/preview/previewFileSamples.test.ts +151 -0
- package/src/preview/previewFileSamples.ts +74 -0
- package/src/preview/previewFixtures.ts +288 -1
- package/src/preview/previewMediaSource.test.ts +62 -0
- package/src/preview/previewMediaSource.ts +91 -0
- package/src/preview/previewMediaUploader.test.ts +61 -0
- package/src/providers/ConversationsProvider.tsx +8 -6
- package/src/providers/types.ts +185 -10
- package/src/quickReply.test.ts +58 -0
- package/src/settings/MessagesWorkspace.tsx +571 -0
- package/src/settings/TopicsForm.tsx +2 -0
- package/src/settings/TranscriptionSettingsForm.test.tsx +81 -0
- package/src/settings/TranscriptionSettingsForm.tsx +190 -0
- package/src/settings/WelcomeFarewellForm.tsx +1 -0
- package/src/settings/WhatsAppCreateTemplateForm.tsx +4 -1
- package/src/settings/WhatsAppTemplateSettingsForm.tsx +5 -2
- package/src/settings/WhatsAppTemplatesSettings.test.tsx +61 -0
- package/src/settings/WhatsAppTemplatesSettings.tsx +22 -2
- package/src/styles.css +858 -0
- package/src/types.ts +64 -1
- package/src/useWaitingNotifications.ts +74 -29
- package/src/workspace/BulkTemplateModal.tsx +132 -0
- package/src/workspace/ConversationPane.tsx +432 -0
- package/src/workspace/ConversationsInboxList.tsx +194 -0
- package/src/workspace/ConversationsWorkspace.tsx +423 -0
- package/src/workspace/index.ts +17 -0
- package/src/workspace/labels.test.ts +17 -0
- package/src/workspace/labels.ts +85 -0
- package/src/workspace/useConversationsInbox.ts +332 -0
- package/dist/chunk-4R6Y43DQ.js +0 -726
- package/dist/chunk-NV2RZ5KT.js +0 -56
- package/dist/types-C0PtaO7S.d.ts +0 -207
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guarda que o preview entrega arquivo do tipo que promete.
|
|
3
|
+
*
|
|
4
|
+
* O defeito que motivou o arquivo: `getDocumentUrl` devolvia sempre a mesma imagem PNG, então
|
|
5
|
+
* "visualizar" num PDF abria um PNG rotulado `application/pdf` e o leitor dizia que o arquivo era
|
|
6
|
+
* inválido. Aqui os bytes são decodificados e conferidos pela assinatura de cada formato — teste que
|
|
7
|
+
* só olhasse o prefixo da data URL passaria com o bug de volta.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, expect, it } from 'bun:test'
|
|
11
|
+
|
|
12
|
+
import { PREVIEW_FILE_SAMPLES, resolvePreviewFileSample } from './previewFileSamples'
|
|
13
|
+
import { PREVIEW_DOCUMENTS } from './previewFixtures'
|
|
14
|
+
|
|
15
|
+
function decode(dataUrl: string): { mimeType: string; bytes: Uint8Array } {
|
|
16
|
+
const [head, payload] = dataUrl.split(',')
|
|
17
|
+
const mimeType = head!.replace(/^data:/, '').replace(/;base64$/, '')
|
|
18
|
+
if (!head!.endsWith(';base64')) {
|
|
19
|
+
return { mimeType, bytes: new TextEncoder().encode(decodeURIComponent(payload!)) }
|
|
20
|
+
}
|
|
21
|
+
return { mimeType, bytes: Uint8Array.from(atob(payload!), (char) => char.charCodeAt(0)) }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function startsWith(bytes: Uint8Array, signature: readonly number[], offset = 0): boolean {
|
|
25
|
+
return signature.every((byte, index) => bytes[offset + index] === byte)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const ascii = (text: string): number[] => [...text].map((char) => char.charCodeAt(0))
|
|
29
|
+
|
|
30
|
+
// Assinatura real de cada formato, não o rótulo do mimeType.
|
|
31
|
+
const SIGNATURES: Record<string, (bytes: Uint8Array) => boolean> = {
|
|
32
|
+
'application/pdf': (bytes) => startsWith(bytes, ascii('%PDF-')),
|
|
33
|
+
'image/png': (bytes) => startsWith(bytes, [0x89, 0x50, 0x4e, 0x47]),
|
|
34
|
+
'image/jpeg': (bytes) => startsWith(bytes, [0xff, 0xd8, 0xff]),
|
|
35
|
+
'image/webp': (bytes) => startsWith(bytes, ascii('RIFF')) && startsWith(bytes, ascii('WEBP'), 8),
|
|
36
|
+
'video/mp4': (bytes) => startsWith(bytes, ascii('ftyp'), 4),
|
|
37
|
+
'audio/mp4': (bytes) => startsWith(bytes, ascii('ftyp'), 4),
|
|
38
|
+
// ID3 quando há tag, ou o sync do primeiro frame MPEG.
|
|
39
|
+
'audio/mpeg': (bytes) => startsWith(bytes, ascii('ID3')) || (bytes[0] === 0xff && (bytes[1]! & 0xe0) === 0xe0),
|
|
40
|
+
'audio/ogg': (bytes) => startsWith(bytes, ascii('OggS')),
|
|
41
|
+
'audio/aac': (bytes) => bytes[0] === 0xff && (bytes[1]! & 0xf0) === 0xf0,
|
|
42
|
+
'text/plain': (bytes) => bytes.length > 0,
|
|
43
|
+
'text/csv': (bytes) => bytes.length > 0,
|
|
44
|
+
// Todo pacote Office é zip: assinatura PK\x03\x04.
|
|
45
|
+
'application/zip': (bytes) => startsWith(bytes, [0x50, 0x4b, 0x03, 0x04]),
|
|
46
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': (bytes) =>
|
|
47
|
+
startsWith(bytes, [0x50, 0x4b, 0x03, 0x04]),
|
|
48
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': (bytes) =>
|
|
49
|
+
startsWith(bytes, [0x50, 0x4b, 0x03, 0x04]),
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe('amostras de arquivo do preview', () => {
|
|
53
|
+
it('toda amostra declara o mimeType que anuncia', () => {
|
|
54
|
+
for (const [mimeType, dataUrl] of Object.entries(PREVIEW_FILE_SAMPLES)) {
|
|
55
|
+
expect(decode(dataUrl).mimeType).toBe(mimeType)
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
for (const [mimeType, isValid] of Object.entries(SIGNATURES)) {
|
|
60
|
+
it(`os bytes de ${mimeType} são realmente desse formato`, () => {
|
|
61
|
+
const sample = PREVIEW_FILE_SAMPLES[mimeType]
|
|
62
|
+
expect(sample).toBeDefined()
|
|
63
|
+
expect(isValid(decode(sample!).bytes)).toBe(true)
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// O PDF sem xref/startxref é o caso que alguns leitores recusam de cara.
|
|
68
|
+
it('o PDF tem tabela xref e termina em %%EOF', () => {
|
|
69
|
+
const text = new TextDecoder().decode(decode(PREVIEW_FILE_SAMPLES['application/pdf']!).bytes)
|
|
70
|
+
|
|
71
|
+
expect(text).toContain('\nxref\n')
|
|
72
|
+
expect(text).toContain('startxref')
|
|
73
|
+
expect(text.trimEnd().endsWith('%%EOF')).toBe(true)
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
describe('resolvePreviewFileSample', () => {
|
|
78
|
+
it('descarta parâmetro do mimeType — áudio de WhatsApp chega com codecs', () => {
|
|
79
|
+
expect(resolvePreviewFileSample('audio/ogg; codecs=opus')).toBe(PREVIEW_FILE_SAMPLES['audio/ogg'])
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
// Tipos aceitos pela Meta sem amostra local (sem encoder amr/3gp nesta máquina): tocam pela
|
|
83
|
+
// família, em vez de devolver bytes de outro formato.
|
|
84
|
+
it('cai na família quando não há amostra do tipo exato', () => {
|
|
85
|
+
expect(resolvePreviewFileSample('audio/amr')).toBe(PREVIEW_FILE_SAMPLES['audio/mpeg'])
|
|
86
|
+
expect(resolvePreviewFileSample('video/3gp')).toBe(PREVIEW_FILE_SAMPLES['video/mp4'])
|
|
87
|
+
expect(resolvePreviewFileSample('image/heic')).toBe(PREVIEW_FILE_SAMPLES['image/png'])
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
// Office legado é OLE2, não zip: servir o docx aqui daria arquivo corrompido no Word. Melhor um
|
|
91
|
+
// texto que abre e se explica.
|
|
92
|
+
it('explica em texto quando não há como forjar o binário', () => {
|
|
93
|
+
const resolved = resolvePreviewFileSample('application/msword', 'procuracao.doc')
|
|
94
|
+
|
|
95
|
+
expect(resolved.startsWith('data:text/plain')).toBe(true)
|
|
96
|
+
expect(decodeURIComponent(resolved)).toContain('procuracao.doc')
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('não devolve vazio para tipo desconhecido', () => {
|
|
100
|
+
expect(resolvePreviewFileSample(undefined).length).toBeGreaterThan(0)
|
|
101
|
+
expect(resolvePreviewFileSample('application/octet-stream').length).toBeGreaterThan(0)
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
describe('biblioteca do preview', () => {
|
|
106
|
+
// A infidelidade que existia: a biblioteca listava só `document`, enquanto o backend linka as
|
|
107
|
+
// cinco espécies de mídia. Foto, vídeo, áudio e sticker apareciam na tela real e não no preview.
|
|
108
|
+
it('lista foto, vídeo, áudio e sticker junto dos documentos', () => {
|
|
109
|
+
const mimeTypes = (PREVIEW_DOCUMENTS['5511944443333'] ?? []).map((document) => document.mimeType)
|
|
110
|
+
|
|
111
|
+
expect(mimeTypes.some((mimeType) => mimeType.startsWith('image/'))).toBe(true)
|
|
112
|
+
expect(mimeTypes.some((mimeType) => mimeType.startsWith('video/'))).toBe(true)
|
|
113
|
+
expect(mimeTypes.some((mimeType) => mimeType.startsWith('audio/'))).toBe(true)
|
|
114
|
+
expect(mimeTypes).toContain('image/webp')
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
// Cada tipo que a Cloud API da Meta aceita para envio de mídia.
|
|
118
|
+
const ACCEPTED_BY_META = [
|
|
119
|
+
'image/jpeg',
|
|
120
|
+
'image/png',
|
|
121
|
+
'image/webp',
|
|
122
|
+
'video/mp4',
|
|
123
|
+
'video/3gp',
|
|
124
|
+
'audio/aac',
|
|
125
|
+
'audio/amr',
|
|
126
|
+
'audio/mpeg',
|
|
127
|
+
'audio/mp4',
|
|
128
|
+
'audio/ogg',
|
|
129
|
+
'application/pdf',
|
|
130
|
+
'application/msword',
|
|
131
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
132
|
+
'application/vnd.ms-excel',
|
|
133
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
134
|
+
'application/vnd.ms-powerpoint',
|
|
135
|
+
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
136
|
+
'text/plain',
|
|
137
|
+
]
|
|
138
|
+
|
|
139
|
+
it('cobre todo tipo aceito pela Meta', () => {
|
|
140
|
+
const present = new Set((PREVIEW_DOCUMENTS['5511944443333'] ?? []).map((document) => document.mimeType))
|
|
141
|
+
const faltando = ACCEPTED_BY_META.filter((mimeType) => !present.has(mimeType))
|
|
142
|
+
|
|
143
|
+
expect(faltando).toEqual([])
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it('todo item da biblioteca abre com bytes não vazios', () => {
|
|
147
|
+
for (const document of PREVIEW_DOCUMENTS['5511944443333'] ?? []) {
|
|
148
|
+
expect(decode(resolvePreviewFileSample(document.mimeType, document.filename)).bytes.length).toBeGreaterThan(0)
|
|
149
|
+
}
|
|
150
|
+
})
|
|
151
|
+
})
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bytes de verdade para o preview abrir.
|
|
3
|
+
*
|
|
4
|
+
* O mock antes devolvia a MESMA imagem PNG para qualquer documento, então clicar em "visualizar" num
|
|
5
|
+
* PDF entregava um PNG rotulado `application/pdf` e o leitor dizia que o arquivo era inválido. Cada
|
|
6
|
+
* amostra aqui é um arquivo mínimo porém legítimo do seu tipo: as de mídia foram geradas com ffmpeg
|
|
7
|
+
* e conferidas com ffprobe, o PDF tem xref/startxref/%%EOF, e os pacotes Office são zips com as
|
|
8
|
+
* partes obrigatórias (`[Content_Types].xml`, `_rels/.rels`, o documento).
|
|
9
|
+
*
|
|
10
|
+
* Não há amostra de `audio/amr`, `video/3gp` nem dos formatos binários legados do Office
|
|
11
|
+
* (`application/msword`, `application/vnd.ms-excel`, `application/vnd.ms-powerpoint`): não há
|
|
12
|
+
* encoder para eles nesta máquina e forjar OLE2 à mão daria arquivo corrompido — exatamente o que
|
|
13
|
+
* este módulo existe para evitar. Esses caem no texto explicativo de `resolvePreviewFileSample`,
|
|
14
|
+
* que abre sem erro e diz o que é.
|
|
15
|
+
*
|
|
16
|
+
* GERADO — não editar à mão.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export const PREVIEW_FILE_SAMPLES: Readonly<Record<string, string>> = {
|
|
20
|
+
'application/pdf':
|
|
21
|
+
'data:application/pdf;base64,JVBERi0xLjQKMSAwIG9iajw8L1R5cGUvQ2F0YWxvZy9QYWdlcyAyIDAgUj4+ZW5kb2JqCjIgMCBvYmo8PC9UeXBlL1BhZ2VzL0tpZHNbMyAwIFJdL0NvdW50IDE+PmVuZG9iagozIDAgb2JqPDwvVHlwZS9QYWdlL1BhcmVudCAyIDAgUi9NZWRpYUJveFswIDAgMjYwIDExMF0vQ29udGVudHMgNCAwIFIvUmVzb3VyY2VzPDwvRm9udDw8L0YxIDUgMCBSPj4+Pj4+ZW5kb2JqCjQgMCBvYmo8PC9MZW5ndGggNjI+PnN0cmVhbQpCVCAvRjEgMTMgVGYgMTggNTUgVGQgKERvY3VtZW50byBkZSBleGVtcGxvIGRvIHByZXZpZXcpIFRqIEVUCmVuZHN0cmVhbWVuZG9iago1IDAgb2JqPDwvVHlwZS9Gb250L1N1YnR5cGUvVHlwZTEvQmFzZUZvbnQvSGVsdmV0aWNhPj5lbmRvYmoKeHJlZgowIDYKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDA5IDAwMDAwIG4gCjAwMDAwMDAwNTIgMDAwMDAgbiAKMDAwMDAwMDEwMSAwMDAwMCBuIAowMDAwMDAwMjExIDAwMDAwIG4gCjAwMDAwMDAzMTcgMDAwMDAgbiAKdHJhaWxlcjw8L1NpemUgNi9Sb290IDEgMCBSPj4Kc3RhcnR4cmVmCjM3OAolJUVPRgo=',
|
|
22
|
+
'image/png':
|
|
23
|
+
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAIAAADYYG7QAAAACXBIWXMAAAABAAAAAQBPJcTWAAAATElEQVR4nO3OMRHAIAAAMbhDWo2hCYPdu/xIh0RB5rPP+JN1O/AlVISKUBEqQkWoCBWhIlSEilARKkJFqAgVoSJUhIpQESpCRagIlRcgDwI25HmPQAAAAABJRU5ErkJggg==',
|
|
24
|
+
'image/jpeg':
|
|
25
|
+
'data:image/jpeg;base64,/9j/4AAQSkZJRgABAgAAAQABAAD//gAQTGF2YzYyLjExLjEwMAD/2wBDAAgEBAQEBAUFBQUFBQYGBgYGBgYGBgYGBgYHBwcICAgHBwcGBgcHCAgICAkJCQgICAgJCQoKCgwMCwsODg4RERT/xABMAAEBAAAAAAAAAAAAAAAAAAAABAEBAQAAAAAAAAAAAAAAAAAAAAYQAQAAAAAAAAAAAAAAAAAAAAARAQAAAAAAAAAAAAAAAAAAAAD/wAARCAAwADADASIAAhEAAxEA/9oADAMBAAIRAxEAPwCUBRpwAAAAAAAAAAAAAAAB/9k=',
|
|
26
|
+
'image/webp': 'data:image/webp;base64,UklGRhoAAABXRUJQVlA4TA0AAAAvAAAAEAcQERGIiP4HAA==',
|
|
27
|
+
'video/mp4':
|
|
28
|
+
'data:video/mp4;base64,AAAAIGZ0eXBpc29tAAACAGlzb21pc28yYXZjMW1wNDEAAANdbW9vdgAAAGxtdmhkAAAAAAAAAAAAAAAAAAAD6AAAASwAAQAAAQAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAoh0cmFrAAAAXHRraGQAAAADAAAAAAAAAAAAAAABAAAAAAAAASwAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAAEAAAABAAAAAAAAkZWR0cwAAABxlbHN0AAAAAAAAAAEAAAEsAAAIAAABAAAAAAIAbWRpYQAAACBtZGhkAAAAAAAAAAAAAAAAAAAoAAAAEABVxAAAAAAALWhkbHIAAAAAAAAAAHZpZGUAAAAAAAAAAAAAAABWaWRlb0hhbmRsZXIAAAABq21pbmYAAAAUdm1oZAAAAAEAAAAAAAAAAAAAACRkaW5mAAAAHGRyZWYAAAAAAAAAAQAAAAx1cmwgAAAAAQAAAWtzdGJsAAAAv3N0c2QAAAAAAAAAAQAAAK9hdmMxAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAEAAQABIAAAASAAAAAAAAAABFUxhdmM2Mi4xMS4xMDAgbGlieDI2NAAAAAAAAAAAAAAAGP//AAAANWF2Y0MBZAAK/+EAGGdkAAqs2UQmwEQAAAMABAAAAwBQPEiWWAEABmjr48siwP34+AAAAAAQcGFzcAAAAAEAAAABAAAAFGJ0cnQAAAAAAABO2gAAAAAAAAAYc3R0cwAAAAAAAAABAAAAAwAABAAAAAAUc3RzcwAAAAAAAAABAAAAAQAAAChjdHRzAAAAAAAAAAMAAAABAAAIAAAAAAEAAAwAAAAAAQAABAAAAAAcc3RzYwAAAAAAAAABAAAAAQAAAAMAAAABAAAAIHN0c3oAAAAAAAAAAAAAAAMAAALbAAAADgAAAAwAAAAUc3RjbwAAAAAAAAABAAADjQAAAGF1ZHRhAAAAWW1ldGEAAAAAAAAAIWhkbHIAAAAAAAAAAG1kaXJhcHBsAAAAAAAAAAAAAAAALGlsc3QAAAAkqXRvbwAAABxkYXRhAAAAAQAAAABMYXZmNjIuMy4xMDAAAAAIZnJlZQAAAv1tZGF0AAACrgYF//+q3EXpvebZSLeWLNgg2SPu73gyNjQgLSBjb3JlIDE2NSByMzIyMiBiMzU2MDVhIC0gSC4yNjQvTVBFRy00IEFWQyBjb2RlYyAtIENvcHlsZWZ0IDIwMDMtMjAyNSAtIGh0dHA6Ly93d3cudmlkZW9sYW4ub3JnL3gyNjQuaHRtbCAtIG9wdGlvbnM6IGNhYmFjPTEgcmVmPTMgZGVibG9jaz0xOjA6MCBhbmFseXNlPTB4MzoweDExMyBtZT1oZXggc3VibWU9NyBwc3k9MSBwc3lfcmQ9MS4wMDowLjAwIG1peGVkX3JlZj0xIG1lX3JhbmdlPTE2IGNocm9tYV9tZT0xIHRyZWxsaXM9MSA4eDhkY3Q9MSBjcW09MCBkZWFkem9uZT0yMSwxMSBmYXN0X3Bza2lwPTEgY2hyb21hX3FwX29mZnNldD0tMiB0aHJlYWRzPTIgbG9va2FoZWFkX3RocmVhZHM9MSBzbGljZWRfdGhyZWFkcz0wIG5yPTAgZGVjaW1hdGU9MSBpbnRlcmxhY2VkPTAgYmx1cmF5X2NvbXBhdD0wIGNvbnN0cmFpbmVkX2ludHJhPTAgYmZyYW1lcz0zIGJfcHlyYW1pZD0yIGJfYWRhcHQ9MSBiX2JpYXM9MCBkaXJlY3Q9MSB3ZWlnaHRiPTEgb3Blbl9nb3A9MCB3ZWlnaHRwPTIga2V5aW50PTI1MCBrZXlpbnRfbWluPTEwIHNjZW5lY3V0PTQwIGludHJhX3JlZnJlc2g9MCByY19sb29rYWhlYWQ9NDAgcmM9Y3JmIG1idHJlZT0xIGNyZj0yMy4wIHFjb21wPTAuNjAgcXBtaW49MCBxcG1heD02OSBxcHN0ZXA9NCBpcF9yYXRpbz0xLjQwIGFxPTE6MS4wMACAAAAAJWWIhAA///7mdfgUoKykhh+DdK6xVynnAWznTQ4fVSgMZ1FwY28AAAAKQZoibEN//qePiAAAAAgBnkF5Df8HfQ==',
|
|
29
|
+
'audio/mpeg':
|
|
30
|
+
'data:audio/mpeg;base64,SUQzBAAAAAAAIlRTU0UAAAAOAAADTGF2ZjYyLjMuMTAwAAAAAAAAAAAAAAD/40jAAAAAAAAAAAAASW5mbwAAAA8AAAAFAAAGwABVVVVVVVVVVVVVVVVVVVVVVVVVgICAgICAgICAgICAgICAgICAgICqqqqqqqqqqqqqqqqqqqqqqqqqqtXV1dXV1dXV1dXV1dXV1dXV1dXV//////////////////////////8AAAAATGF2YzYyLjExAAAAAAAAAAAAAAAAJALAAAAAAAAABsDR4nNFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/40jEAAAAA0gAAAAATEFNRTMuMTAwVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/40jEAAAAA0gAAAAATEFNRTMuMTAwVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/40jEAAAAA0gAAAAATEFNRTMuMTAwVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/40jEAAAAA0gAAAAATEFNRTMuMTAwVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/40jEAAAAA0gAAAAATEFNRTMuMTAwVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVU=',
|
|
31
|
+
'audio/ogg':
|
|
32
|
+
'data:audio/ogg;base64,T2dnUwACAAAAAAAAAABXDTmmAAAAAKMnDKEBE09wdXNIZWFkAQE4AYC7AAAAAABPZ2dTAAAAAAAAAAAAAFcNOaYBAAAAZlPuKwE9T3B1c1RhZ3MMAAAATGF2ZjYyLjMuMTAwAQAAAB0AAABlbmNvZGVyPUxhdmM2Mi4xMS4xMDAgbGlib3B1c09nZ1MABLgmAAAAAAAAVw05pgIAAAA3uL6lCw8REhISEhISEhISeAvkwTbsxY2MSUaZpCaweAfJcifhROpV8fDA1d3pUmB4B8l5yMlXwKISI/rvZ/NkwOB4B8l5yMlXwKISI/rvZ/NkwOB4B8l5yMlXwKISI/rvZ/NkwOB4B8l5yMlXwKISI/rvZ/NkwOB4B8l5yMlXwKISI/rvZ/NkwOB4B8l5yMlXwKISI/rvZ/NkwOB4B8l5yMlXwKISI/rvZ/NkwOB4B8l5yMlXwKISI/rvZ/NkwOB4B8l5yMlXwKISI/rvZ/NkwOA=',
|
|
33
|
+
'audio/mp4':
|
|
34
|
+
'data:audio/mp4;base64,AAAAHGZ0eXBNNEEgAAACAE00QSBpc29taXNvMgAAAAhmcmVlAAAAQW1kYXTeAgBMYXZjNjIuMTEuMTAwAAIwQA4BGCAHARggBwEYIAcBGCAHARggBwEYIAcBGCAHARggBwEYIAcAAAMibW9vdgAAAGxtdmhkAAAAAAAAAAAAAAAAAAAD6AAAAMgAAQAAAQAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAk10cmFrAAAAXHRraGQAAAADAAAAAAAAAAAAAAABAAAAAAAAAMgAAAAAAAAAAAAAAAEBAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAkZWR0cwAAABxlbHN0AAAAAAAAAAEAAADIAAAEAAABAAAAAAHFbWRpYQAAACBtZGhkAAAAAAAAAAAAAAAAAACsRAAAJnRVxAAAAAAALWhkbHIAAAAAAAAAAHNvdW4AAAAAAAAAAAAAAABTb3VuZEhhbmRsZXIAAAABcG1pbmYAAAAQc21oZAAAAAAAAAAAAAAAJGRpbmYAAAAcZHJlZgAAAAAAAAABAAAADHVybCAAAAABAAABNHN0YmwAAABqc3RzZAAAAAAAAAABAAAAWm1wNGEAAAAAAAAAAQAAAAAAAAAAAAEAEAAAAACsRAAAAAAANmVzZHMAAAAAA4CAgCUAAQAEgICAF0AVAAAAAAB9AAAAB/oFgICABRIIVuUABoCAgAECAAAAIHN0dHMAAAAAAAAAAgAAAAkAAAQAAAAAAQAAAnQAAAAcc3RzYwAAAAAAAAABAAAAAQAAAAoAAAABAAAAPHN0c3oAAAAAAAAAAAAAAAoAAAAVAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAAFHN0Y28AAAAAAAAAAQAAACwAAAAac2dwZAEAAAByb2xsAAAAAgAAAAH//wAAABxzYmdwAAAAAHJvbGwAAAABAAAACgAAAAEAAABhdWR0YQAAAFltZXRhAAAAAAAAACFoZGxyAAAAAAAAAABtZGlyYXBwbAAAAAAAAAAAAAAAACxpbHN0AAAAJKl0b28AAAAcZGF0YQAAAAEAAAAATGF2ZjYyLjMuMTAw',
|
|
35
|
+
'audio/aac':
|
|
36
|
+
'data:audio/aac;base64,//FQQAOf/N4CAExhdmM2Mi4xMS4xMDAAAjBADv/xUEABf/wBGCAH//FQQAF//AEYIAf/8VBAAX/8ARggB//xUEABf/wBGCAH//FQQAF//AEYIAf/8VBAAX/8ARggB//xUEABf/wBGCAH//FQQAF//AEYIAf/8VBAAX/8ARggBw==',
|
|
37
|
+
'text/plain': 'data:text/plain;base64,TGlzdGEgZGUgY29tcHJhcwotIGFycm96IDJrZwotIG9sZW8gMXVuCg==',
|
|
38
|
+
'text/csv': 'data:text/csv;base64,cHJvZHV0byxxdWFudGlkYWRlLHByZWNvCmFycm96LDIsMjQuOTAKb2xlbywxLDcuNTAK',
|
|
39
|
+
'application/zip':
|
|
40
|
+
'data:application/zip;base64,UEsDBBQAAAAIANFO/FwtmGOxGwAAABkAAAARAAAAY29tcHJvdmFudGUtMS50eHRLzs8tKMovS8wrSVVISVUoSExPzE3NK8nnAgBQSwMEFAAAAAgA0U78XCftV8YWAAAAFAAAABEAAABjb21wcm92YW50ZS0yLnR4dCtOTS/NS8lXSM7PLSjKL0vMK0nlAgBQSwECFAMUAAAACADRTvxcLZhjsRsAAAAZAAAAEQAAAAAAAAAAAAAAgAEAAAAAY29tcHJvdmFudGUtMS50eHRQSwECFAMUAAAACADRTvxcJ+1XxhYAAAAUAAAAEQAAAAAAAAAAAAAAgAFKAAAAY29tcHJvdmFudGUtMi50eHRQSwUGAAAAAAIAAgB+AAAAjwAAAAAA',
|
|
41
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
|
|
42
|
+
'data:application/vnd.openxmlformats-officedocument.wordprocessingml.document;base64,UEsDBBQAAAAIANFO/Fx5bjPX6AAAAK0BAAATAAAAW0NvbnRlbnRfVHlwZXNdLnhtbH1QyU7DMBD9FWuuKHHggBCK0wPLETiUDxjZk8SqN3nc0v49Tlt6QIXjzFv1+tXeO7GjzDYGBbdtB4KCjsaGScHn+rV5AMEFg0EXAyk4EMNq6NeHRCyqNrCCuZT0KCXrmTxyGxOFiowxeyz1zJNMqDc4kbzrunupYygUSlMWDxj6Zxpx64p42df3qUcmxyCeTsQlSwGm5KzGUnG5C+ZXSnNOaKvyyOHZJr6pBJBXExbk74Cz7r0Ok60h8YG5vKGvLPkVs5Em6q2vyvZ/mys94zhaTRf94pZy1MRcF/euvSAebfjpL49zD99QSwMEFAAAAAgA0U78XJv9N+qtAAAAKQEAAAsAAABfcmVscy8ucmVsc43POw7CMAwG4KtE3mlaBoRQ0y4IqSsqB7ASN61oHkrCo7cnAwNFDIy2f3+W6/ZpZnanECdnBVRFCYysdGqyWsClP232wGJCq3B2lgQsFKFt6jPNmPJKHCcfWTZsFDCm5A+cRzmSwVg4TzZPBhcMplwGzT3KK2ri27Lc8fBpwNpknRIQOlUB6xdP/9huGCZJRydvhmz6ceIrkWUMmpKAhwuKq3e7yCzwpuarF5sXUEsDBBQAAAAIANFO/FyraIuoqgAAAOcAAAARAAAAd29yZC9kb2N1bWVudC54bWxFjksOwjAMBa8SZU9TWCBUNWWBxAngAKExbaTGjpzQz+1JyoLNPFt+Grm9rn4SM3B0hFoeq1oKwJ6sw0HL5+N+uEgRk0FrJkLQcoMor127NJb6jwdMIgswNouWY0qhUSr2I3gTKwqA+fYm9ibllQe1ENvA1EOM2e8ndarrs/LGoSzKF9mtZCjggtTdCBObRMKCgBV8mPJIIjDMDpZWlU4h7ww7fx71/7H7AlBLAQIUAxQAAAAIANFO/Fx5bjPX6AAAAK0BAAATAAAAAAAAAAAAAACAAQAAAABbQ29udGVudF9UeXBlc10ueG1sUEsBAhQDFAAAAAgA0U78XJv9N+qtAAAAKQEAAAsAAAAAAAAAAAAAAIABGQEAAF9yZWxzLy5yZWxzUEsBAhQDFAAAAAgA0U78XKtoi6iqAAAA5wAAABEAAAAAAAAAAAAAAIAB7wEAAHdvcmQvZG9jdW1lbnQueG1sUEsFBgAAAAADAAMAuQAAAMgCAAAAAA==',
|
|
43
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
|
|
44
|
+
'data:application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;base64,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',
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const FAMILY_FALLBACK: Readonly<Record<string, string>> = {
|
|
48
|
+
image: 'image/png',
|
|
49
|
+
video: 'video/mp4',
|
|
50
|
+
audio: 'audio/mpeg',
|
|
51
|
+
text: 'text/plain',
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* A data URL que o preview abre para um documento.
|
|
56
|
+
*
|
|
57
|
+
* Ordem: tipo exato → família (`audio/amr` toca como mp3, `video/3gp` como mp4) → nota em texto.
|
|
58
|
+
* A nota fecha o caso dos tipos sem amostra: abre num leitor de texto e explica, em vez de baixar
|
|
59
|
+
* um binário quebrado com extensão de Word.
|
|
60
|
+
*/
|
|
61
|
+
export function resolvePreviewFileSample(mimeType: string | undefined, filename?: string): string {
|
|
62
|
+
const normalized = (mimeType ?? '').split(';')[0]!.trim().toLowerCase()
|
|
63
|
+
const exact = PREVIEW_FILE_SAMPLES[normalized]
|
|
64
|
+
if (exact) return exact
|
|
65
|
+
|
|
66
|
+
const family = FAMILY_FALLBACK[normalized.split('/')[0] ?? '']
|
|
67
|
+
const byFamily = family ? PREVIEW_FILE_SAMPLES[family] : undefined
|
|
68
|
+
if (byFamily) return byFamily
|
|
69
|
+
|
|
70
|
+
const note = `Preview do SDK: nao existe amostra binaria valida para ${normalized || 'tipo desconhecido'}${
|
|
71
|
+
filename ? ` (${filename})` : ''
|
|
72
|
+
}. Na tela real este arquivo vem do storage.`
|
|
73
|
+
return `data:text/plain;charset=utf-8,${encodeURIComponent(note)}`
|
|
74
|
+
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { MessagePayload } from '../types'
|
|
8
|
-
import type { ConversationSummary } from '../providers/types'
|
|
8
|
+
import type { ConversationDocument, ConversationSummary } from '../providers/types'
|
|
9
9
|
|
|
10
10
|
// Datas fixas: fixture com data relativa ao relógio faz o mesmo cenário renderizar diferente a
|
|
11
11
|
// cada execução, e separadores de dia deixam de ser verificáveis.
|
|
@@ -71,6 +71,24 @@ export const PREVIEW_CONVERSATIONS: readonly ConversationSummary[] = [
|
|
|
71
71
|
unread: 1,
|
|
72
72
|
currentState: 'list_import',
|
|
73
73
|
},
|
|
74
|
+
{
|
|
75
|
+
// Cobre TODO tipo que o composer aceita (DEFAULT_ACCEPTED_FILE_TYPES: image/*, video/*,
|
|
76
|
+
// audio/*, .pdf, .doc, .docx, .xls, .xlsx, .zip) mais sticker. Existe para que cada ramo do
|
|
77
|
+
// MediaRenderer e cada ícone/cor do FileIcon apareçam em algum lugar — ramo sem fixture é ramo
|
|
78
|
+
// que ninguém olha até quebrar em produção.
|
|
79
|
+
id: '5511944443333',
|
|
80
|
+
whatsappNumber: '5511944443333',
|
|
81
|
+
clientName: 'Rita Documentos',
|
|
82
|
+
lastContent: 'segue a planilha do pedido',
|
|
83
|
+
lastDirection: 'inbound',
|
|
84
|
+
lastAt: at('15:10:00'),
|
|
85
|
+
lastInboundAt: at('15:10:00'),
|
|
86
|
+
mode: 'human',
|
|
87
|
+
assignedUserId: 'agent-1',
|
|
88
|
+
waitingHuman: false,
|
|
89
|
+
unread: 3,
|
|
90
|
+
currentState: 'human_handling',
|
|
91
|
+
},
|
|
74
92
|
]
|
|
75
93
|
|
|
76
94
|
export const PREVIEW_MESSAGES: Readonly<Record<string, readonly MessagePayload[]>> = {
|
|
@@ -150,4 +168,273 @@ export const PREVIEW_MESSAGES: Readonly<Record<string, readonly MessagePayload[]
|
|
|
150
168
|
timestamp: at('13:31:00'),
|
|
151
169
|
},
|
|
152
170
|
],
|
|
171
|
+
// Um tipo por mensagem, na ordem em que o MediaRenderer os trata.
|
|
172
|
+
'5511944443333': [
|
|
173
|
+
{
|
|
174
|
+
id: 'fixture-doc-image',
|
|
175
|
+
type: 'image',
|
|
176
|
+
mediaId: 'preview-image-1',
|
|
177
|
+
mimeType: 'image/png',
|
|
178
|
+
caption: 'foto da prateleira',
|
|
179
|
+
direction: 'inbound',
|
|
180
|
+
sender: 'customer',
|
|
181
|
+
timestamp: at('15:00:00'),
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
id: 'fixture-doc-video',
|
|
185
|
+
type: 'video',
|
|
186
|
+
mediaId: 'preview-video-1',
|
|
187
|
+
mimeType: 'video/mp4',
|
|
188
|
+
direction: 'inbound',
|
|
189
|
+
sender: 'customer',
|
|
190
|
+
timestamp: at('15:01:00'),
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
id: 'fixture-doc-audio',
|
|
194
|
+
type: 'audio',
|
|
195
|
+
mediaId: 'preview-audio-2',
|
|
196
|
+
mimeType: 'audio/ogg',
|
|
197
|
+
direction: 'inbound',
|
|
198
|
+
sender: 'customer',
|
|
199
|
+
timestamp: at('15:02:00'),
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
id: 'fixture-doc-sticker',
|
|
203
|
+
type: 'sticker',
|
|
204
|
+
mediaId: 'preview-sticker-1',
|
|
205
|
+
mimeType: 'image/webp',
|
|
206
|
+
direction: 'inbound',
|
|
207
|
+
sender: 'customer',
|
|
208
|
+
timestamp: at('15:03:00'),
|
|
209
|
+
},
|
|
210
|
+
// Os cinco ramos do FileIcon: pdf, doc, xls, zip e o genérico do fallback.
|
|
211
|
+
{
|
|
212
|
+
id: 'fixture-doc-pdf',
|
|
213
|
+
type: 'document',
|
|
214
|
+
uploadId: 'preview/documentos/nota-fiscal.pdf',
|
|
215
|
+
filename: 'nota-fiscal.pdf',
|
|
216
|
+
mimeType: 'application/pdf',
|
|
217
|
+
sizeBytes: 184_320,
|
|
218
|
+
direction: 'inbound',
|
|
219
|
+
sender: 'customer',
|
|
220
|
+
timestamp: at('15:04:00'),
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
id: 'fixture-doc-docx',
|
|
224
|
+
type: 'document',
|
|
225
|
+
uploadId: 'preview/documentos/contrato.docx',
|
|
226
|
+
filename: 'contrato.docx',
|
|
227
|
+
mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
228
|
+
sizeBytes: 42_112,
|
|
229
|
+
direction: 'inbound',
|
|
230
|
+
sender: 'customer',
|
|
231
|
+
timestamp: at('15:05:00'),
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
id: 'fixture-doc-doc',
|
|
235
|
+
type: 'document',
|
|
236
|
+
uploadId: 'preview/documentos/procuracao.doc',
|
|
237
|
+
filename: 'procuracao.doc',
|
|
238
|
+
mimeType: 'application/msword',
|
|
239
|
+
sizeBytes: 31_744,
|
|
240
|
+
direction: 'inbound',
|
|
241
|
+
sender: 'customer',
|
|
242
|
+
timestamp: at('15:06:00'),
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
id: 'fixture-doc-xlsx',
|
|
246
|
+
type: 'document',
|
|
247
|
+
uploadId: 'preview/documentos/pedido.xlsx',
|
|
248
|
+
filename: 'pedido.xlsx',
|
|
249
|
+
mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
250
|
+
sizeBytes: 15_872,
|
|
251
|
+
direction: 'inbound',
|
|
252
|
+
sender: 'customer',
|
|
253
|
+
timestamp: at('15:07:00'),
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
id: 'fixture-doc-xls',
|
|
257
|
+
type: 'document',
|
|
258
|
+
uploadId: 'preview/documentos/tabela-antiga.xls',
|
|
259
|
+
filename: 'tabela-antiga.xls',
|
|
260
|
+
mimeType: 'application/vnd.ms-excel',
|
|
261
|
+
sizeBytes: 9_216,
|
|
262
|
+
direction: 'inbound',
|
|
263
|
+
sender: 'customer',
|
|
264
|
+
timestamp: at('15:08:00'),
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
id: 'fixture-doc-zip',
|
|
268
|
+
type: 'document',
|
|
269
|
+
uploadId: 'preview/documentos/comprovantes.zip',
|
|
270
|
+
filename: 'comprovantes.zip',
|
|
271
|
+
mimeType: 'application/zip',
|
|
272
|
+
sizeBytes: 2_355_200,
|
|
273
|
+
direction: 'inbound',
|
|
274
|
+
sender: 'customer',
|
|
275
|
+
timestamp: at('15:09:00'),
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
id: 'fixture-doc-txt',
|
|
279
|
+
type: 'document',
|
|
280
|
+
uploadId: 'preview/documentos/lista-compras.txt',
|
|
281
|
+
filename: 'lista-compras.txt',
|
|
282
|
+
mimeType: 'text/plain',
|
|
283
|
+
sizeBytes: 1_024,
|
|
284
|
+
direction: 'inbound',
|
|
285
|
+
sender: 'customer',
|
|
286
|
+
timestamp: at('15:10:00'),
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
id: 'fixture-doc-csv',
|
|
290
|
+
type: 'document',
|
|
291
|
+
uploadId: 'preview/documentos/planilha-do-pedido.csv',
|
|
292
|
+
filename: 'planilha-do-pedido.csv',
|
|
293
|
+
mimeType: 'text/csv',
|
|
294
|
+
sizeBytes: 2_048,
|
|
295
|
+
direction: 'inbound',
|
|
296
|
+
sender: 'customer',
|
|
297
|
+
timestamp: at('15:11:00'),
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
id: 'fixture-doc-pptx',
|
|
301
|
+
type: 'document',
|
|
302
|
+
uploadId: 'preview/documentos/apresentacao.pptx',
|
|
303
|
+
filename: 'apresentacao.pptx',
|
|
304
|
+
mimeType: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
305
|
+
sizeBytes: 512_000,
|
|
306
|
+
direction: 'inbound',
|
|
307
|
+
sender: 'customer',
|
|
308
|
+
timestamp: at('15:12:00'),
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
id: 'fixture-doc-ppt',
|
|
312
|
+
type: 'document',
|
|
313
|
+
uploadId: 'preview/documentos/antiga.ppt',
|
|
314
|
+
filename: 'antiga.ppt',
|
|
315
|
+
mimeType: 'application/vnd.ms-powerpoint',
|
|
316
|
+
sizeBytes: 348_160,
|
|
317
|
+
direction: 'inbound',
|
|
318
|
+
sender: 'customer',
|
|
319
|
+
timestamp: at('15:13:00'),
|
|
320
|
+
},
|
|
321
|
+
// Mídia enviada COMO arquivo: aí a Meta manda o nome, então é onde a extensão de música e de
|
|
322
|
+
// vídeo aparece na lista. Mandada como `image`/`audio`/`video` (acima e abaixo) ela chega sem
|
|
323
|
+
// nome nenhum, e o rótulo vira o id da mídia — como em produção.
|
|
324
|
+
{
|
|
325
|
+
id: 'fixture-doc-mp3',
|
|
326
|
+
type: 'document',
|
|
327
|
+
uploadId: 'preview/documentos/musica-do-anuncio.mp3',
|
|
328
|
+
filename: 'musica-do-anuncio.mp3',
|
|
329
|
+
mimeType: 'audio/mpeg',
|
|
330
|
+
sizeBytes: 3_145_728,
|
|
331
|
+
direction: 'inbound',
|
|
332
|
+
sender: 'customer',
|
|
333
|
+
timestamp: at('15:14:00'),
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
id: 'fixture-doc-m4a',
|
|
337
|
+
type: 'document',
|
|
338
|
+
uploadId: 'preview/documentos/recado.m4a',
|
|
339
|
+
filename: 'recado.m4a',
|
|
340
|
+
mimeType: 'audio/mp4',
|
|
341
|
+
sizeBytes: 486_400,
|
|
342
|
+
direction: 'inbound',
|
|
343
|
+
sender: 'customer',
|
|
344
|
+
timestamp: at('15:15:00'),
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
id: 'fixture-doc-mp4-file',
|
|
348
|
+
type: 'document',
|
|
349
|
+
uploadId: 'preview/documentos/video-do-produto.mp4',
|
|
350
|
+
filename: 'video-do-produto.mp4',
|
|
351
|
+
mimeType: 'video/mp4',
|
|
352
|
+
sizeBytes: 8_388_608,
|
|
353
|
+
direction: 'inbound',
|
|
354
|
+
sender: 'customer',
|
|
355
|
+
timestamp: at('15:16:00'),
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
id: 'fixture-doc-jpg',
|
|
359
|
+
type: 'document',
|
|
360
|
+
uploadId: 'preview/documentos/foto-do-recibo.jpg',
|
|
361
|
+
filename: 'foto-do-recibo.jpg',
|
|
362
|
+
mimeType: 'image/jpeg',
|
|
363
|
+
sizeBytes: 262_144,
|
|
364
|
+
direction: 'inbound',
|
|
365
|
+
sender: 'customer',
|
|
366
|
+
timestamp: at('15:17:00'),
|
|
367
|
+
},
|
|
368
|
+
// As espécies restantes que a Meta aceita, sem nome de arquivo — fecham a lista de tipos
|
|
369
|
+
// aceitos e exercitam o fallback de ícone pela família do mimeType.
|
|
370
|
+
{
|
|
371
|
+
id: 'fixture-media-jpeg',
|
|
372
|
+
type: 'image',
|
|
373
|
+
mediaId: 'preview-image-jpeg',
|
|
374
|
+
mimeType: 'image/jpeg',
|
|
375
|
+
sizeBytes: 138_240,
|
|
376
|
+
direction: 'inbound',
|
|
377
|
+
sender: 'customer',
|
|
378
|
+
timestamp: at('15:18:00'),
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
id: 'fixture-media-3gp',
|
|
382
|
+
type: 'video',
|
|
383
|
+
mediaId: 'preview-video-3gp',
|
|
384
|
+
mimeType: 'video/3gp',
|
|
385
|
+
sizeBytes: 921_600,
|
|
386
|
+
direction: 'inbound',
|
|
387
|
+
sender: 'customer',
|
|
388
|
+
timestamp: at('15:19:00'),
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
id: 'fixture-media-aac',
|
|
392
|
+
type: 'audio',
|
|
393
|
+
mediaId: 'preview-audio-aac',
|
|
394
|
+
mimeType: 'audio/aac',
|
|
395
|
+
sizeBytes: 76_800,
|
|
396
|
+
direction: 'inbound',
|
|
397
|
+
sender: 'customer',
|
|
398
|
+
timestamp: at('15:20:00'),
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
id: 'fixture-media-amr',
|
|
402
|
+
type: 'audio',
|
|
403
|
+
mediaId: 'preview-audio-amr',
|
|
404
|
+
mimeType: 'audio/amr',
|
|
405
|
+
sizeBytes: 24_576,
|
|
406
|
+
direction: 'inbound',
|
|
407
|
+
sender: 'customer',
|
|
408
|
+
timestamp: at('15:21:00'),
|
|
409
|
+
},
|
|
410
|
+
],
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* A biblioteca de arquivos da conversa, como o backend a devolveria. Deriva das mensagens acima em
|
|
415
|
+
* vez de repetir os dados: fixture duplicada divergiria na primeira edição, e o painel passaria a
|
|
416
|
+
* mostrar arquivo que a thread não tem.
|
|
417
|
+
*
|
|
418
|
+
* Entram as CINCO espécies de mídia, na mesma lista de `extractMediaDescriptor` no backend — não só
|
|
419
|
+
* `document`. Filtrar por documento era infidelidade grave do preview: `IngestInboundMedia` linka
|
|
420
|
+
* foto, vídeo, áudio e sticker na mesma tabela, então na tela real eles aparecem na biblioteca e no
|
|
421
|
+
* preview não apareciam.
|
|
422
|
+
*/
|
|
423
|
+
const LIBRARY_MEDIA_TYPES = new Set(['document', 'image', 'video', 'audio', 'sticker'])
|
|
424
|
+
|
|
425
|
+
export const PREVIEW_DOCUMENTS: Readonly<Record<string, readonly ConversationDocument[]>> = {
|
|
426
|
+
'5511944443333': (PREVIEW_MESSAGES['5511944443333'] ?? [])
|
|
427
|
+
.filter((message) => LIBRARY_MEDIA_TYPES.has(message.type))
|
|
428
|
+
.map((message) => ({
|
|
429
|
+
// Mídia não tem `uploadId` na mensagem: o backend deriva a key do id da mídia na Meta, e o
|
|
430
|
+
// caminho abaixo imita esse formato para o download assinado receber algo do mesmo feitio.
|
|
431
|
+
id: message.uploadId ?? (message.mediaId ? `preview/inbound/${message.mediaId}` : message.id),
|
|
432
|
+
// Só documento chega nomeado. Para os outros o backend salva o id da mídia como rótulo —
|
|
433
|
+
// manter isso aqui é o que faz o ícone por família do mimeType ser exercitado de verdade.
|
|
434
|
+
filename: message.filename ?? message.mediaId ?? message.id,
|
|
435
|
+
mimeType: message.mimeType ?? 'application/octet-stream',
|
|
436
|
+
sizeBytes: message.sizeBytes ?? 0,
|
|
437
|
+
source: message.sender,
|
|
438
|
+
linkedAt: message.timestamp,
|
|
439
|
+
})),
|
|
153
440
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guarda o resolvedor de mídia do preview.
|
|
3
|
+
*
|
|
4
|
+
* As asserções comparam com `previewFileUrl(tipo)` em vez de com a data URL da amostra: o bun também
|
|
5
|
+
* tem `URL.createObjectURL`, então o retorno é `blob:` aqui como no navegador, e blob URL não expõe
|
|
6
|
+
* o conteúdo para comparar. O que este teste trava é o MAPEAMENTO — mensagem → amostra do tipo certo
|
|
7
|
+
* —, enquanto os bytes de cada tipo são conferidos em `previewFileSamples.test.ts`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, expect, it } from 'bun:test'
|
|
11
|
+
|
|
12
|
+
import { createPreviewMediaResolver, previewFileUrl } from './previewMediaSource'
|
|
13
|
+
import type { MessagePayload } from '../types'
|
|
14
|
+
|
|
15
|
+
const resolve = createPreviewMediaResolver()
|
|
16
|
+
|
|
17
|
+
function message(partial: Partial<MessagePayload>): MessagePayload {
|
|
18
|
+
return {
|
|
19
|
+
id: 'msg-1',
|
|
20
|
+
type: 'image',
|
|
21
|
+
direction: 'inbound',
|
|
22
|
+
sender: 'customer',
|
|
23
|
+
timestamp: '2026-07-26T15:00:00.000Z',
|
|
24
|
+
...partial,
|
|
25
|
+
} as MessagePayload
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe('createPreviewMediaResolver', () => {
|
|
29
|
+
it('resolve mídia pelo mediaId, achando o tipo na biblioteca do fixture', async () => {
|
|
30
|
+
// preview-image-1 é a foto png da thread da Rita.
|
|
31
|
+
expect(await resolve(message({ mediaId: 'preview-image-1' }))).toBe(previewFileUrl('image/png'))
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('resolve documento pelo uploadId', async () => {
|
|
35
|
+
const resolved = await resolve(message({ type: 'document', uploadId: 'preview/documentos/nota-fiscal.pdf' }))
|
|
36
|
+
|
|
37
|
+
expect(resolved).toBe(previewFileUrl('application/pdf'))
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
// O sticker é webp e o áudio de voz é opus: dois tipos que o painel antes nem listava.
|
|
41
|
+
it('resolve sticker e áudio', async () => {
|
|
42
|
+
expect(await resolve(message({ mediaId: 'preview-sticker-1' }))).toBe(previewFileUrl('image/webp'))
|
|
43
|
+
expect(await resolve(message({ mediaId: 'preview-audio-2' }))).toBe(previewFileUrl('audio/ogg'))
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
// Mídia que não está na biblioteca ainda tem de abrir: cai no mimeType da própria mensagem.
|
|
47
|
+
it('usa o mimeType da mensagem quando o id é desconhecido', async () => {
|
|
48
|
+
const resolved = await resolve(message({ mediaId: 'nunca-visto', mimeType: 'image/jpeg' }))
|
|
49
|
+
|
|
50
|
+
expect(resolved).toBe(previewFileUrl('image/jpeg'))
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
// Sem referência nenhuma o MediaRenderer não deve receber URL — devolver algo aqui faria a bolha
|
|
54
|
+
// mostrar arquivo que a mensagem não tem.
|
|
55
|
+
it('devolve null para mensagem sem uploadId nem mediaId', async () => {
|
|
56
|
+
expect(await resolve(message({ type: 'text' }))).toBeNull()
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('reaproveita a mesma URL para o mesmo tipo', () => {
|
|
60
|
+
expect(previewFileUrl('application/pdf')).toBe(previewFileUrl('application/pdf'))
|
|
61
|
+
})
|
|
62
|
+
})
|