@adatechnology/conversations-ui 0.1.0-rc.5 → 0.1.0-rc.7
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/chunk-TGTBMMFC.js +1707 -0
- package/dist/index.d.ts +209 -22
- package/dist/index.js +243 -590
- package/dist/preview/index.d.ts +153 -9
- package/dist/preview/index.js +520 -38
- package/dist/{types-C2Yexi8A.d.ts → types-B5C1DLu1.d.ts} +70 -2
- package/package.json +2 -2
- package/src/Avatar.tsx +13 -2
- package/src/ConversationHeader.tsx +18 -0
- package/src/ConversationListItem.tsx +18 -2
- package/src/DocumentsLibrary.tsx +322 -0
- package/src/EmojiPicker.tsx +69 -55
- package/src/FileIcon.test.ts +46 -1
- package/src/FileIcon.tsx +76 -10
- package/src/InteractiveMessage.tsx +126 -0
- package/src/Lightbox.tsx +18 -3
- package/src/MessageBubble.tsx +31 -4
- package/src/MessageComposer.tsx +36 -5
- package/src/WhatsAppMessageEditor.tsx +28 -4
- package/src/emojiCatalog.test.ts +35 -0
- package/src/emojiCatalog.ts +189 -0
- package/src/index.ts +25 -12
- package/src/lib/createMediaUrlResolver.ts +33 -0
- package/src/preview/AudioRecorderButton.tsx +117 -0
- package/src/preview/ConversationPreview.tsx +184 -15
- package/src/preview/MediaTypesPreview.tsx +87 -0
- package/src/preview/audioRecorderFormat.test.ts +67 -0
- package/src/preview/conversationPreviewFailures.test.ts +64 -0
- package/src/preview/createMockConversationsApi.ts +62 -8
- package/src/preview/createPreviewWebhookClient.ts +28 -1
- package/src/preview/index.ts +16 -2
- package/src/preview/mediaTypeOf.test.ts +15 -0
- package/src/preview/mockDocumentsSearch.test.ts +57 -0
- package/src/preview/previewFileSamples.test.ts +151 -0
- package/src/preview/previewFileSamples.ts +74 -0
- package/src/preview/previewFixtures.ts +140 -8
- package/src/preview/previewMediaSource.test.ts +62 -0
- package/src/preview/previewMediaSource.ts +91 -0
- package/src/providers/types.ts +17 -0
- package/src/settings/WhatsAppCreateTemplateForm.tsx +3 -1
- package/src/types.ts +38 -1
- package/dist/chunk-YWITIIHD.js +0 -728
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Como o preview entrega mídia para a UI abrir.
|
|
3
|
+
*
|
|
4
|
+
* Separado de `previewFileSamples` (que é gerado) porque aqui está a parte que depende do navegador:
|
|
5
|
+
* **blob URL, não data URL**. O Chrome bloqueia navegação de topo para `data:` desde a v60, então
|
|
6
|
+
* `window.open(dataUrl)` — que é o que os botões "visualizar" e "baixar" fazem — abre aba em branco
|
|
7
|
+
* mesmo com bytes perfeitamente válidos. Em `src` de `<img>`/`<video>` a data URL funcionaria; no
|
|
8
|
+
* `window.open` não, e o mesmo `getDocumentUrl` alimenta os dois.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { MessagePayload } from '../types'
|
|
12
|
+
import type { ResolveMediaUrl } from '../MediaRenderer'
|
|
13
|
+
import { resolvePreviewFileSample } from './previewFileSamples'
|
|
14
|
+
import { PREVIEW_DOCUMENTS } from './previewFixtures'
|
|
15
|
+
|
|
16
|
+
// Uma blob URL por tipo, reaproveitada: cada `createObjectURL` retém o blob até um `revokeObjectURL`
|
|
17
|
+
// que ninguém chamaria, e a lista redesenha a cada filtro digitado.
|
|
18
|
+
const blobUrlCache = new Map<string, string>()
|
|
19
|
+
|
|
20
|
+
function toBlob(dataUrl: string): Blob {
|
|
21
|
+
const [head, payload] = dataUrl.split(',')
|
|
22
|
+
const mimeType = head!
|
|
23
|
+
.replace(/^data:/, '')
|
|
24
|
+
.replace(/;base64$/, '')
|
|
25
|
+
.replace(/;charset=.*$/, '')
|
|
26
|
+
|
|
27
|
+
if (!head!.endsWith(';base64')) {
|
|
28
|
+
return new Blob([decodeURIComponent(payload!)], { type: mimeType || 'text/plain' })
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const binary = atob(payload!)
|
|
32
|
+
const bytes = new Uint8Array(binary.length)
|
|
33
|
+
for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index)
|
|
34
|
+
return new Blob([bytes], { type: mimeType })
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** URL que o preview pode abrir em aba nova. Cai na data URL fora do navegador (teste, SSR). */
|
|
38
|
+
export function previewFileUrl(mimeType: string | undefined, filename?: string): string {
|
|
39
|
+
const dataUrl = resolvePreviewFileSample(mimeType, filename)
|
|
40
|
+
if (typeof URL === 'undefined' || typeof URL.createObjectURL !== 'function') return dataUrl
|
|
41
|
+
|
|
42
|
+
const cached = blobUrlCache.get(dataUrl)
|
|
43
|
+
if (cached) return cached
|
|
44
|
+
|
|
45
|
+
const blobUrl = URL.createObjectURL(toBlob(dataUrl))
|
|
46
|
+
blobUrlCache.set(dataUrl, blobUrl)
|
|
47
|
+
return blobUrl
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A mesma amostra em base64 cru, como o proxy de mídia do backend a devolveria.
|
|
52
|
+
*
|
|
53
|
+
* Existe porque `getMediaProxyUrl` é o caminho da mídia AINDA NÃO ingerida (só existe o id na Meta),
|
|
54
|
+
* e o contrato pede `{ mimeType, data }` — não URL. Sem isto o mock devolvia o PNG 1x1 para
|
|
55
|
+
* qualquer id, e vídeo e áudio da thread apareciam quebrados apesar de haver amostra válida.
|
|
56
|
+
*/
|
|
57
|
+
export function previewFileBase64(mimeType: string | undefined, filename?: string): { mimeType: string; data: string } {
|
|
58
|
+
const dataUrl = resolvePreviewFileSample(mimeType, filename)
|
|
59
|
+
const [head, payload] = dataUrl.split(',')
|
|
60
|
+
const declared = head!
|
|
61
|
+
.replace(/^data:/, '')
|
|
62
|
+
.replace(/;base64$/, '')
|
|
63
|
+
.replace(/;charset=.*$/, '')
|
|
64
|
+
|
|
65
|
+
if (head!.endsWith(';base64')) return { mimeType: declared, data: payload! }
|
|
66
|
+
return { mimeType: declared, data: btoa(decodeURIComponent(payload!)) }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* O `onResolveMediaUrl` que o `MessageBubble` espera.
|
|
71
|
+
*
|
|
72
|
+
* Sem ele, foto, vídeo e áudio da thread ficam parados no placeholder para sempre — o
|
|
73
|
+
* `MediaRenderer` só resolve `uploadId`/`mediaId` por esta porta, de propósito, para o pacote nunca
|
|
74
|
+
* chamar endpoint fixo. O preview não tinha resolvedor nenhum, então nenhuma mídia carregava.
|
|
75
|
+
*/
|
|
76
|
+
export function createPreviewMediaResolver(): ResolveMediaUrl {
|
|
77
|
+
const byId = new Map<string, { mimeType: string; filename: string }>()
|
|
78
|
+
for (const documents of Object.values(PREVIEW_DOCUMENTS)) {
|
|
79
|
+
for (const document of documents) {
|
|
80
|
+
byId.set(document.id, { mimeType: document.mimeType, filename: document.filename })
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return async (message: MessagePayload): Promise<string | null> => {
|
|
85
|
+
const reference = message.uploadId ?? (message.mediaId ? `preview/inbound/${message.mediaId}` : undefined)
|
|
86
|
+
if (!reference) return null
|
|
87
|
+
|
|
88
|
+
const known = byId.get(reference)
|
|
89
|
+
return previewFileUrl(known?.mimeType ?? message.mimeType, known?.filename ?? message.filename)
|
|
90
|
+
}
|
|
91
|
+
}
|
package/src/providers/types.ts
CHANGED
|
@@ -36,6 +36,16 @@ export interface ListDocumentsParams {
|
|
|
36
36
|
sortDirection?: 'asc' | 'desc'
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
/** Arquivo na biblioteca da empresa: o mesmo da conversa, mais de qual conversa veio. */
|
|
40
|
+
export interface CompanyDocument extends ConversationDocument {
|
|
41
|
+
conversationId: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface CompanyDocumentPage {
|
|
45
|
+
documents: CompanyDocument[]
|
|
46
|
+
total: number
|
|
47
|
+
}
|
|
48
|
+
|
|
39
49
|
export interface ConversationDocumentPage {
|
|
40
50
|
documents: ConversationDocument[]
|
|
41
51
|
total: number
|
|
@@ -93,6 +103,13 @@ export interface ConversationsApi {
|
|
|
93
103
|
* lote em vez de oferecer um botão que falha.
|
|
94
104
|
*/
|
|
95
105
|
downloadDocumentsArchive?(conversationId: string, uploadIds: readonly string[]): Promise<Blob>
|
|
106
|
+
/**
|
|
107
|
+
* Biblioteca de TODAS as conversas, para uma tela de Documentos fora do atendimento.
|
|
108
|
+
*
|
|
109
|
+
* Opcional por capacidade: host que só expõe anexo dentro da conversa não implementa, e o
|
|
110
|
+
* componente de biblioteca simplesmente não é usável — melhor que uma tela que sempre erra.
|
|
111
|
+
*/
|
|
112
|
+
getAllDocuments?(params?: ListDocumentsParams): Promise<CompanyDocumentPage>
|
|
96
113
|
getMediaProxyUrl(mediaId: string): Promise<{ mimeType: string; data: string }>
|
|
97
114
|
|
|
98
115
|
/**
|
|
@@ -24,6 +24,7 @@ export interface WhatsAppCreateTemplateFormLabels {
|
|
|
24
24
|
sectionDescription: string
|
|
25
25
|
nameLabel: string
|
|
26
26
|
nameHint: string
|
|
27
|
+
namePlaceholder: string
|
|
27
28
|
categoryLabel: string
|
|
28
29
|
languageLabel: string
|
|
29
30
|
headerLabel: string
|
|
@@ -57,6 +58,7 @@ const DEFAULT_LABELS: WhatsAppCreateTemplateFormLabels = {
|
|
|
57
58
|
sectionDescription: 'Envia um template para aprovação da Meta.',
|
|
58
59
|
nameLabel: 'Nome do template',
|
|
59
60
|
nameHint: 'Somente letras minúsculas, números e underscore.',
|
|
61
|
+
namePlaceholder: 'reengajamento_cliente',
|
|
60
62
|
categoryLabel: 'Categoria',
|
|
61
63
|
languageLabel: 'Idioma',
|
|
62
64
|
headerLabel: 'Cabeçalho',
|
|
@@ -142,7 +144,7 @@ export function WhatsAppCreateTemplateForm({
|
|
|
142
144
|
type="text"
|
|
143
145
|
value={value.name}
|
|
144
146
|
onChange={(e) => set('name', e.target.value.toLowerCase().replace(/[^a-z0-9_]/g, '_'))}
|
|
145
|
-
placeholder=
|
|
147
|
+
placeholder={labels.namePlaceholder}
|
|
146
148
|
className="w-full border border-gray-200 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 rounded-xl px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-400 focus:border-transparent transition-all"
|
|
147
149
|
required
|
|
148
150
|
/>
|
package/src/types.ts
CHANGED
|
@@ -20,9 +20,46 @@ export interface ConversationsFeatures {
|
|
|
20
20
|
darkMode?: boolean
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Recorte do bloco `interactive` da Meta que a UI precisa para desenhar o menu. Fica solto (e não
|
|
25
|
+
* espelhando o contrato inteiro) porque o que chega do banco é o payload cru já enviado ao
|
|
26
|
+
* WhatsApp: qualquer campo que a UI não conheça é ignorado, nunca causa erro de render.
|
|
27
|
+
*/
|
|
28
|
+
export interface InteractiveOption {
|
|
29
|
+
id: string
|
|
30
|
+
title: string
|
|
31
|
+
description?: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface InteractiveSection {
|
|
35
|
+
title?: string
|
|
36
|
+
rows?: InteractiveOption[]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface InteractivePayload {
|
|
40
|
+
type?: 'button' | 'list' | string
|
|
41
|
+
header?: { text?: string }
|
|
42
|
+
body?: { text?: string }
|
|
43
|
+
footer?: { text?: string }
|
|
44
|
+
action?: {
|
|
45
|
+
/** Rótulo do botão que abre a lista — só existe em `type: 'list'`. */
|
|
46
|
+
button?: string
|
|
47
|
+
sections?: InteractiveSection[]
|
|
48
|
+
buttons?: { reply?: InteractiveOption }[]
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Como o cliente respondeu a um menu: por botão ou por item de lista. */
|
|
53
|
+
export type InteractiveSelection = {
|
|
54
|
+
readonly kind: 'button' | 'list'
|
|
55
|
+
readonly option: InteractiveOption
|
|
56
|
+
}
|
|
57
|
+
|
|
23
58
|
export interface MessagePayload {
|
|
24
59
|
id: string
|
|
25
|
-
type: 'text' | 'image' | 'video' | 'audio' | 'document' | 'sticker' | 'template'
|
|
60
|
+
type: 'text' | 'image' | 'video' | 'audio' | 'document' | 'sticker' | 'template' | 'interactive'
|
|
61
|
+
/** Payload cru da mensagem. Em `type: 'interactive'`, carrega o menu que o cliente vê. */
|
|
62
|
+
payload?: InteractivePayload | null
|
|
26
63
|
content?: string
|
|
27
64
|
caption?: string
|
|
28
65
|
mediaUrl?: string
|