@adatechnology/conversations-ui 0.1.0-rc.2 → 0.1.0-rc.20
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-ZDURDZTM.js → chunk-2AYDBWNE.js} +14 -0
- package/dist/chunk-OIDAIVCH.js +1985 -0
- package/dist/flows/index.d.ts +14 -2
- package/dist/flows/index.js +117 -39
- package/dist/index.d.ts +729 -136
- package/dist/index.js +1418 -1104
- package/dist/preview/index.d.ts +373 -0
- package/dist/preview/index.js +1247 -0
- package/dist/styles.css +198 -0
- package/dist/types-O7kMP1Yn.d.ts +416 -0
- package/package.json +10 -3
- package/src/AudioRecorderButton.test.tsx +30 -0
- package/src/AudioRecorderButton.tsx +248 -0
- package/src/Avatar.tsx +30 -4
- package/src/ChannelIcon.tsx +87 -0
- package/src/ConversationContextPanel.tsx +127 -0
- package/src/ConversationDocumentsPanel.tsx +425 -0
- package/src/ConversationHeader.tsx +257 -0
- package/src/ConversationListItem.tsx +54 -7
- package/src/ConversationLocalesProvider.tsx +16 -0
- package/src/ConversationRow.tsx +137 -0
- package/src/DateDivider.tsx +16 -3
- package/src/DocumentsLibrary.tsx +322 -0
- package/src/EmojiPicker.tsx +69 -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 +143 -0
- package/src/Lightbox.tsx +18 -3
- package/src/MediaRenderer.tsx +64 -21
- package/src/MessageBubble.tsx +54 -5
- package/src/MessageComposer.test.tsx +35 -0
- package/src/MessageComposer.tsx +146 -19
- package/src/RichMessageComposer.test.tsx +83 -0
- package/src/RichMessageComposer.tsx +380 -0
- package/src/Wallpaper.test.tsx +21 -0
- package/src/Wallpaper.tsx +54 -6
- package/src/WhatsAppMessageEditor.tsx +28 -4
- package/src/WindowExpiredNotice.tsx +57 -0
- package/src/audioRecorderFormat.test.ts +67 -0
- package/src/conversationChannel.test.ts +53 -0
- package/src/conversationChannel.ts +146 -0
- package/src/conversationTranscript.test.ts +65 -0
- package/src/conversationTranscript.ts +64 -0
- package/src/conversationWindow.test.ts +90 -0
- package/src/conversationWindow.ts +78 -0
- package/src/documentTypeLabel.test.ts +57 -0
- package/src/emojiCatalog.test.ts +35 -0
- package/src/emojiCatalog.ts +189 -0
- package/src/flows/FlowGroupHeader.tsx +12 -2
- package/src/flows/FlowMapCanvas.tsx +17 -14
- package/src/flows/FlowMapNode.tsx +3 -1
- package/src/flows/FlowNodeCard.tsx +22 -4
- package/src/flows/FlowNodePanel.tsx +132 -35
- package/src/flows/FlowPalette.tsx +6 -2
- package/src/flows/FlowWhatsAppPreview.tsx +14 -3
- package/src/flows/flowGraph.ts +5 -5
- package/src/flows/labels.ts +5 -0
- package/src/hooks/useConversationActions.ts +56 -0
- package/src/hooks/useConversationDocuments.ts +15 -9
- package/src/hooks/useConversationList.ts +15 -9
- package/src/hooks/useConversationMessages.ts +2 -2
- package/src/index.ts +129 -16
- package/src/lib/cn.test.ts +29 -0
- package/src/lib/cn.ts +15 -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/phone.ts +34 -0
- package/src/preview/ConversationPreview.tsx +325 -0
- package/src/preview/MediaTypesPreview.tsx +87 -0
- package/src/preview/conversationPreviewFailures.test.ts +64 -0
- package/src/preview/createMockConversationsApi.ts +271 -0
- package/src/preview/createMockSSEProvider.ts +40 -0
- package/src/preview/createPreviewBridgeClient.test.ts +92 -0
- package/src/preview/createPreviewBridgeClient.ts +88 -0
- package/src/preview/createPreviewWebhookClient.test.ts +105 -0
- package/src/preview/createPreviewWebhookClient.ts +131 -0
- package/src/preview/index.ts +61 -0
- package/src/preview/mediaTypeOf.test.ts +15 -0
- package/src/preview/mockDocumentsSearch.test.ts +57 -0
- package/src/preview/mockEventSource.ts +53 -0
- package/src/preview/preview.test.ts +177 -0
- package/src/preview/previewFileSamples.test.ts +151 -0
- package/src/preview/previewFileSamples.ts +74 -0
- package/src/preview/previewFixtures.ts +440 -0
- package/src/preview/previewMediaSource.test.ts +62 -0
- package/src/preview/previewMediaSource.ts +91 -0
- package/src/preview/previewStore.ts +193 -0
- package/src/preview/startPreviewScript.ts +60 -0
- package/src/providers/types.ts +163 -11
- package/src/quickReply.test.ts +58 -0
- package/src/settings/WhatsAppCreateTemplateForm.tsx +3 -1
- package/src/settings/WhatsAppTemplatesSettings.tsx +106 -0
- package/src/styles.css +136 -0
- package/src/types.ts +46 -1
- package/src/useDarkMode.ts +26 -0
- package/src/useIsNarrow.ts +29 -0
- package/src/useWaitingNotifications.ts +74 -29
|
@@ -72,7 +72,9 @@ export function FlowPalette({ onAdd, labels: labelsOverride, actionOptions }: Fl
|
|
|
72
72
|
onClick={() => setSubmenu(submenu === 'question' ? null : 'question')}
|
|
73
73
|
className="w-full flex items-center justify-between gap-2 px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700"
|
|
74
74
|
>
|
|
75
|
-
<span className="flex items-center gap-2"
|
|
75
|
+
<span className="flex items-center gap-2">
|
|
76
|
+
<MessageCircleQuestion size={15} className="text-blue-500" /> {labels.palette.question}
|
|
77
|
+
</span>
|
|
76
78
|
<ChevronRight size={13} className="text-gray-400" />
|
|
77
79
|
</button>
|
|
78
80
|
{submenu === 'question' && (
|
|
@@ -113,7 +115,9 @@ export function FlowPalette({ onAdd, labels: labelsOverride, actionOptions }: Fl
|
|
|
113
115
|
onClick={() => setSubmenu(submenu === 'action' ? null : 'action')}
|
|
114
116
|
className="w-full flex items-center justify-between gap-2 px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700"
|
|
115
117
|
>
|
|
116
|
-
<span className="flex items-center gap-2"
|
|
118
|
+
<span className="flex items-center gap-2">
|
|
119
|
+
<Zap size={15} className="text-orange-500" /> {labels.palette.action}
|
|
120
|
+
</span>
|
|
117
121
|
<ChevronRight size={13} className="text-gray-400" />
|
|
118
122
|
</button>
|
|
119
123
|
{submenu === 'action' && (
|
|
@@ -13,7 +13,11 @@ export interface FlowWhatsAppPreviewProps {
|
|
|
13
13
|
// opções, botões (≤3) ou lista (4+) — para o editor mostrar exatamente o que o cliente verá.
|
|
14
14
|
// Consome a mesma bolha/formatação do pacote (parseWhatsAppFormatting, T6.5) — T7.3 elimina a
|
|
15
15
|
// duplicação de estilo que existia entre este preview e o MessageBubble do bot.
|
|
16
|
-
export function FlowWhatsAppPreview({
|
|
16
|
+
export function FlowWhatsAppPreview({
|
|
17
|
+
body,
|
|
18
|
+
options,
|
|
19
|
+
labels = DEFAULT_FLOW_EDITOR_LABELS.nodePanel,
|
|
20
|
+
}: FlowWhatsAppPreviewProps) {
|
|
17
21
|
if (!body && (!options || options.length === 0)) {
|
|
18
22
|
return <p className="text-xs text-gray-400 dark:text-gray-500 italic px-1">{labels.previewPlaceholder}</p>
|
|
19
23
|
}
|
|
@@ -25,7 +29,11 @@ export function FlowWhatsAppPreview({ body, options, labels = DEFAULT_FLOW_EDITO
|
|
|
25
29
|
<div className="rounded-xl bg-[#e5ddd5] dark:bg-gray-900 p-3 space-y-1.5">
|
|
26
30
|
<div className="max-w-[85%]">
|
|
27
31
|
<div className="rounded-lg rounded-tl-none bg-white dark:bg-gray-700 px-3 py-2 text-sm text-gray-900 dark:text-gray-100 shadow-sm whitespace-pre-wrap break-words">
|
|
28
|
-
{body ?
|
|
32
|
+
{body ? (
|
|
33
|
+
parseWhatsAppFormatting(body)
|
|
34
|
+
) : (
|
|
35
|
+
<span className="italic text-gray-400">{labels.previewEmptyBody}</span>
|
|
36
|
+
)}
|
|
29
37
|
{hasOptions && !usesButtons && (
|
|
30
38
|
<div className="mt-2 -mx-3 -mb-2 border-t border-gray-100 dark:border-gray-600">
|
|
31
39
|
<div className="flex items-center justify-center gap-1.5 py-2 text-sm font-medium text-cyan-600 dark:text-cyan-400">
|
|
@@ -38,7 +46,10 @@ export function FlowWhatsAppPreview({ body, options, labels = DEFAULT_FLOW_EDITO
|
|
|
38
46
|
{hasOptions && usesButtons && (
|
|
39
47
|
<div className="mt-1 space-y-1">
|
|
40
48
|
{options!.map(([id, label]) => (
|
|
41
|
-
<div
|
|
49
|
+
<div
|
|
50
|
+
key={id}
|
|
51
|
+
className="rounded-lg bg-white dark:bg-gray-700 py-1.5 text-center text-sm font-medium text-cyan-600 dark:text-cyan-400 shadow-sm"
|
|
52
|
+
>
|
|
42
53
|
{label || <span className="italic text-gray-400">{labels.previewEmptyOption}</span>}
|
|
43
54
|
</div>
|
|
44
55
|
))}
|
package/src/flows/flowGraph.ts
CHANGED
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
FlowNodeData,
|
|
14
14
|
FlowGraphData,
|
|
15
15
|
} from '@adatechnology/meta-whatsapp-contracts'
|
|
16
|
+
import { FLOW_ACTION_KIND } from '@adatechnology/meta-whatsapp-contracts'
|
|
16
17
|
|
|
17
18
|
export type {
|
|
18
19
|
FlowNodeType,
|
|
@@ -28,11 +29,10 @@ export const CONDITION_OPERATORS: FlowConditionOperator[] = ['>', '>=', '<', '<=
|
|
|
28
29
|
|
|
29
30
|
// Kinds de ação genéricos que o pacote conhece de fábrica — o host pode registrar quaisquer
|
|
30
31
|
// outros via `actionKindLabels`/`actionKinds` nos componentes (ver FlowPalette, labels.ts).
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
} as const
|
|
32
|
+
// Reexporta o vocabulário do contrato em vez de redeclarar os literais: o editor oferece na
|
|
33
|
+
// paleta exatamente os `actionKind` que o backend sabe interpretar, e duas listas separadas
|
|
34
|
+
// divergiriam em silêncio (um nó publicável que nenhum handler atende).
|
|
35
|
+
export const BUILT_IN_ACTION_KINDS = FLOW_ACTION_KIND
|
|
36
36
|
|
|
37
37
|
// Destinos "flow:<key>" são saltos para outro fluxo, resolvidos pelo motor do host. Duplicado
|
|
38
38
|
// (em vez de importado do contracts) de propósito: são três linhas triviais e importá-las como
|
package/src/flows/labels.ts
CHANGED
|
@@ -47,6 +47,8 @@ export interface FlowEditorLabels {
|
|
|
47
47
|
conditionTrue: string
|
|
48
48
|
conditionFalse: string
|
|
49
49
|
conditionVariableMissing: string
|
|
50
|
+
media: string
|
|
51
|
+
mediaUnavailable: string
|
|
50
52
|
}
|
|
51
53
|
palette: {
|
|
52
54
|
title: string
|
|
@@ -87,6 +89,7 @@ export const DEFAULT_FLOW_EDITOR_LABELS: FlowEditorLabels = {
|
|
|
87
89
|
handoff: 'Encaminhar para atendimento',
|
|
88
90
|
rate_limited_handoff: 'Encaminhar (limite de simulações atingido)',
|
|
89
91
|
send_product_list: 'Enviar catálogo de produtos',
|
|
92
|
+
send_media: 'Enviar arquivos da biblioteca',
|
|
90
93
|
},
|
|
91
94
|
conditionOperatorLabels: {
|
|
92
95
|
'>': 'maior que',
|
|
@@ -143,6 +146,8 @@ export const DEFAULT_FLOW_EDITOR_LABELS: FlowEditorLabels = {
|
|
|
143
146
|
conditionTrue: 'Se verdadeiro →',
|
|
144
147
|
conditionFalse: 'Se falso →',
|
|
145
148
|
conditionVariableMissing: 'Se a variável ainda não foi coletada →',
|
|
149
|
+
media: 'Arquivos enviados neste ponto',
|
|
150
|
+
mediaUnavailable: 'A biblioteca de arquivos não está disponível neste painel.',
|
|
146
151
|
},
|
|
147
152
|
palette: {
|
|
148
153
|
title: 'Adicionar ao fluxo',
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { useMemo } from 'react'
|
|
2
|
+
import { useConversations } from '../providers/ConversationsProvider'
|
|
3
|
+
import type { ConversationTemplate } from '../providers/types'
|
|
4
|
+
|
|
5
|
+
export interface UseConversationActionsResult {
|
|
6
|
+
/** `undefined` quando a API do host não implementa a operação — a UI esconde a afordância. */
|
|
7
|
+
takeover: (() => Promise<void>) | undefined
|
|
8
|
+
release: (() => Promise<void>) | undefined
|
|
9
|
+
finalize: (() => Promise<void>) | undefined
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Ações de atendimento de UMA conversa, já ligadas ao id.
|
|
14
|
+
*
|
|
15
|
+
* Separado de `useConversationMessages` porque assumir e devolver conversa também acontece a
|
|
16
|
+
* partir da lista, onde nenhuma thread está aberta — embutir nas mensagens obrigaria a carregar
|
|
17
|
+
* a thread inteira só para desenhar um botão na linha.
|
|
18
|
+
*/
|
|
19
|
+
export function useConversationActions(conversationId: string): UseConversationActionsResult {
|
|
20
|
+
const context = useConversations()
|
|
21
|
+
if (!context) {
|
|
22
|
+
throw new Error('useConversationActions requires an ancestor <ConversationsProvider>')
|
|
23
|
+
}
|
|
24
|
+
const { api } = context
|
|
25
|
+
|
|
26
|
+
return useMemo(
|
|
27
|
+
() => ({
|
|
28
|
+
takeover: api.takeover ? () => api.takeover!(conversationId) : undefined,
|
|
29
|
+
release: api.release ? () => api.release!(conversationId) : undefined,
|
|
30
|
+
finalize: api.finalize ? () => api.finalize!(conversationId) : undefined,
|
|
31
|
+
}),
|
|
32
|
+
[api, conversationId],
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface UseInboxActionsResult {
|
|
37
|
+
markAllRead: (() => Promise<void>) | undefined
|
|
38
|
+
listTemplates: (() => Promise<ConversationTemplate[]>) | undefined
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Ações que valem para a caixa inteira, sem conversa selecionada. */
|
|
42
|
+
export function useInboxActions(): UseInboxActionsResult {
|
|
43
|
+
const context = useConversations()
|
|
44
|
+
if (!context) {
|
|
45
|
+
throw new Error('useInboxActions requires an ancestor <ConversationsProvider>')
|
|
46
|
+
}
|
|
47
|
+
const { api } = context
|
|
48
|
+
|
|
49
|
+
return useMemo(
|
|
50
|
+
() => ({
|
|
51
|
+
markAllRead: api.markAllRead ? () => api.markAllRead!() : undefined,
|
|
52
|
+
listTemplates: api.listTemplates ? () => api.listTemplates!() : undefined,
|
|
53
|
+
}),
|
|
54
|
+
[api],
|
|
55
|
+
)
|
|
56
|
+
}
|
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
import { useConversations } from '../providers/ConversationsProvider'
|
|
2
2
|
import { useAsyncResource } from './useAsyncResource'
|
|
3
|
-
import
|
|
3
|
+
import { documentsOf, totalOf } from '../lib/paginated'
|
|
4
|
+
import type { ConversationDocument, ListDocumentsParams } from '../providers/types'
|
|
4
5
|
|
|
5
|
-
export
|
|
6
|
-
search?: string
|
|
7
|
-
page?: number
|
|
8
|
-
}
|
|
6
|
+
export type UseConversationDocumentsParams = ListDocumentsParams
|
|
9
7
|
|
|
10
8
|
export interface UseConversationDocumentsResult {
|
|
11
9
|
documents: ConversationDocument[]
|
|
10
|
+
/** Total no servidor. Cai para o tamanho da página quando a API devolve só o array. */
|
|
11
|
+
total: number
|
|
12
12
|
loading: boolean
|
|
13
13
|
error: Error | undefined
|
|
14
14
|
refetch: () => Promise<void>
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
// Aceita `undefined` como "ainda não buscar", igual a `useConversationRealtime`: a lista de anexos
|
|
18
|
+
// é consulta extra, e quem só abre o painel sob demanda não deve pagar por ela em toda conversa.
|
|
17
19
|
export function useConversationDocuments(
|
|
18
|
-
conversationId: string,
|
|
20
|
+
conversationId: string | undefined,
|
|
19
21
|
params?: UseConversationDocumentsParams,
|
|
20
22
|
): UseConversationDocumentsResult {
|
|
21
23
|
const context = useConversations()
|
|
@@ -25,9 +27,13 @@ export function useConversationDocuments(
|
|
|
25
27
|
const { api } = context
|
|
26
28
|
|
|
27
29
|
const { data, loading, error, refetch } = useAsyncResource(
|
|
28
|
-
() => api.getDocuments(conversationId, params),
|
|
29
|
-
[conversationId, params?.search, params?.page],
|
|
30
|
+
() => (conversationId ? api.getDocuments(conversationId, params) : Promise.resolve([])),
|
|
31
|
+
[conversationId, params?.search, params?.page, params?.limit, params?.source, params?.sortDirection],
|
|
30
32
|
)
|
|
31
33
|
|
|
32
|
-
|
|
34
|
+
if (data === undefined) {
|
|
35
|
+
return { documents: [], total: 0, loading, error, refetch }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return { documents: documentsOf(data), total: totalOf(data), loading, error, refetch }
|
|
33
39
|
}
|
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
import { useConversations } from '../providers/ConversationsProvider'
|
|
2
2
|
import { useAsyncResource } from './useAsyncResource'
|
|
3
|
-
import
|
|
3
|
+
import { conversationsOf, totalOf } from '../lib/paginated'
|
|
4
|
+
import type { ConversationSummary, ListConversationsParams } from '../providers/types'
|
|
4
5
|
|
|
5
|
-
export
|
|
6
|
-
page?: number
|
|
7
|
-
limit?: number
|
|
8
|
-
waitingHuman?: boolean
|
|
9
|
-
search?: string
|
|
10
|
-
}
|
|
6
|
+
export type UseConversationListParams = ListConversationsParams
|
|
11
7
|
|
|
12
8
|
export interface UseConversationListResult {
|
|
13
9
|
conversations: ConversationSummary[]
|
|
10
|
+
/** Total no servidor. Cai para o tamanho da página quando a API devolve só o array. */
|
|
11
|
+
total: number
|
|
14
12
|
loading: boolean
|
|
15
13
|
error: Error | undefined
|
|
16
14
|
refetch: () => Promise<void>
|
|
@@ -23,10 +21,18 @@ export function useConversationList(params?: UseConversationListParams): UseConv
|
|
|
23
21
|
}
|
|
24
22
|
const { api } = context
|
|
25
23
|
|
|
24
|
+
// `filters` é objeto novo a cada render do host; serializar evita refetch em laço sem obrigar
|
|
25
|
+
// o consumidor a memoizar — omissão que só apareceria como loop de rede em produção.
|
|
26
|
+
const filtersKey = JSON.stringify(params?.filters ?? {})
|
|
27
|
+
|
|
26
28
|
const { data, loading, error, refetch } = useAsyncResource(
|
|
27
29
|
() => api.fetchConversations(params),
|
|
28
|
-
[params?.page, params?.limit, params?.waitingHuman, params?.search],
|
|
30
|
+
[params?.page, params?.limit, params?.waitingHuman, params?.search, filtersKey],
|
|
29
31
|
)
|
|
30
32
|
|
|
31
|
-
|
|
33
|
+
if (data === undefined) {
|
|
34
|
+
return { conversations: [], total: 0, loading, error, refetch }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return { conversations: conversationsOf(data), total: totalOf(data), loading, error, refetch }
|
|
32
38
|
}
|
|
@@ -10,7 +10,7 @@ export interface UseConversationMessagesResult {
|
|
|
10
10
|
refetch: () => Promise<void>
|
|
11
11
|
sendMessage: (text: string) => Promise<MessagePayload>
|
|
12
12
|
sendMedia: (data: { base64: string; mimeType: string; filename: string; caption?: string }) => Promise<MessagePayload>
|
|
13
|
-
sendTemplate: (data: { templateName
|
|
13
|
+
sendTemplate: (data: { templateName?: string; languageCode?: string; bodyParams?: string[] }) => Promise<void>
|
|
14
14
|
markRead: () => Promise<void>
|
|
15
15
|
}
|
|
16
16
|
|
|
@@ -51,7 +51,7 @@ export function useConversationMessages(
|
|
|
51
51
|
)
|
|
52
52
|
|
|
53
53
|
const sendTemplate = useCallback(
|
|
54
|
-
async (templateData: { templateName
|
|
54
|
+
async (templateData: { templateName?: string; languageCode?: string; bodyParams?: string[] }) => {
|
|
55
55
|
await api.sendTemplate(conversationId, templateData)
|
|
56
56
|
await refetch()
|
|
57
57
|
},
|
package/src/index.ts
CHANGED
|
@@ -1,26 +1,105 @@
|
|
|
1
1
|
export { MessageBubble } from './MessageBubble'
|
|
2
|
+
export { InteractiveMessage, DEFAULT_INTERACTIVE_MESSAGE_LABELS } from './InteractiveMessage'
|
|
2
3
|
export { ConversationWallpaper } from './Wallpaper'
|
|
3
4
|
export { ConversationLocalesProvider, useConversationLocales } from './ConversationLocalesProvider'
|
|
4
5
|
export { AudioPlayer } from './AudioPlayer'
|
|
5
|
-
export { EmojiPicker } from './EmojiPicker'
|
|
6
|
-
export {
|
|
7
|
-
export {
|
|
6
|
+
export { EmojiPicker, DEFAULT_EMOJI_PICKER_LABELS } from './EmojiPicker'
|
|
7
|
+
export { EMOJI_CATEGORIES, searchEmojis } from './emojiCatalog'
|
|
8
|
+
export { MessageComposer, DEFAULT_MESSAGE_COMPOSER_LABELS, DEFAULT_ACCEPTED_FILE_TYPES } from './MessageComposer'
|
|
9
|
+
export { RichMessageComposer, RICH_COMPOSER_ACTION, DEFAULT_RICH_COMPOSER_TOOLTIPS } from './RichMessageComposer'
|
|
10
|
+
export type {
|
|
11
|
+
RichMessageComposerProps,
|
|
12
|
+
RichComposerAction,
|
|
13
|
+
RichComposerTooltips,
|
|
14
|
+
RichComposerQuickReply,
|
|
15
|
+
RichComposerVariable,
|
|
16
|
+
RichMessageComposerHandle,
|
|
17
|
+
} from './RichMessageComposer'
|
|
18
|
+
export {
|
|
19
|
+
AudioRecorderButton,
|
|
20
|
+
DEFAULT_AUDIO_RECORDER_BUTTON_LABELS,
|
|
21
|
+
DEFAULT_MAX_RECORDING_MILLISECONDS,
|
|
22
|
+
} from './AudioRecorderButton'
|
|
23
|
+
export type { AudioRecorderButtonProps, AudioRecorderButtonLabels } from './AudioRecorderButton'
|
|
24
|
+
export { WhatsAppMessageEditor, DEFAULT_WHATSAPP_MESSAGE_EDITOR_LABELS } from './WhatsAppMessageEditor'
|
|
8
25
|
export { SimpleEmojiPicker } from './SimpleEmojiPicker'
|
|
9
26
|
export { DateDivider } from './DateDivider'
|
|
10
|
-
export { Avatar } from './Avatar'
|
|
11
|
-
export { ConversationListItem } from './ConversationListItem'
|
|
27
|
+
export { Avatar, DEFAULT_AVATAR_LABELS } from './Avatar'
|
|
28
|
+
export { ConversationListItem, DEFAULT_CONVERSATION_LIST_ITEM_LABELS } from './ConversationListItem'
|
|
12
29
|
export { ToastProvider, useToast, toast } from './Toast'
|
|
13
30
|
|
|
14
31
|
export { StatusTicks } from './StatusTicks'
|
|
15
|
-
export { Lightbox } from './Lightbox'
|
|
32
|
+
export { Lightbox, DEFAULT_LIGHTBOX_LABELS } from './Lightbox'
|
|
16
33
|
export { MediaRenderer } from './MediaRenderer'
|
|
17
34
|
export { FileIcon } from './FileIcon'
|
|
18
35
|
export { MessageText } from './MessageText'
|
|
19
36
|
export { MessageTimestamp } from './MessageTimestamp'
|
|
20
37
|
export { MessageTail } from './MessageTail'
|
|
21
38
|
|
|
22
|
-
|
|
39
|
+
// Operação da inbox: janela de 24h da Meta, linha com ações e painéis do atendimento. Regras da
|
|
40
|
+
// plataforma e do ofício de atender, não de um produto — por isso moram aqui.
|
|
41
|
+
export { CONVERSATION_WINDOW, WINDOW_FILTERS, windowOf, formatStalledFor } from './conversationWindow'
|
|
42
|
+
export type { WindowOfParams } from './conversationWindow'
|
|
43
|
+
// Canal de origem: capacidades por plataforma (janela de sessão, reabertura, tipo de identificador).
|
|
44
|
+
export {
|
|
45
|
+
CONVERSATION_CHANNEL,
|
|
46
|
+
DEFAULT_CONVERSATION_CHANNEL,
|
|
47
|
+
CHANNEL_CAPABILITIES,
|
|
48
|
+
channelFiltersFor,
|
|
49
|
+
CHANNEL_FILTER_ALL,
|
|
50
|
+
REOPEN_MECHANISM,
|
|
51
|
+
HANDLE_KIND,
|
|
52
|
+
capabilitiesOf,
|
|
53
|
+
formatContactHandle,
|
|
54
|
+
contactFlag,
|
|
55
|
+
} from './conversationChannel'
|
|
56
|
+
export type {
|
|
57
|
+
ConversationChannel,
|
|
58
|
+
ChannelCapabilities,
|
|
59
|
+
ChannelFilter,
|
|
60
|
+
ChannelFilterOption,
|
|
61
|
+
ReopenMechanism,
|
|
62
|
+
HandleKind,
|
|
63
|
+
FormatContactHandleParams,
|
|
64
|
+
} from './conversationChannel'
|
|
65
|
+
export type { ConversationWindow } from './conversationWindow'
|
|
66
|
+
export { ConversationRow } from './ConversationRow'
|
|
67
|
+
export { ChannelIcon, CHANNEL_BRAND_COLOR } from './ChannelIcon'
|
|
68
|
+
export type { ChannelIconProps } from './ChannelIcon'
|
|
69
|
+
export type { ConversationRowProps, ConversationRowClassNames } from './ConversationRow'
|
|
70
|
+
export { ConversationHeader, DEFAULT_CONVERSATION_HEADER_LABELS } from './ConversationHeader'
|
|
71
|
+
export type {
|
|
72
|
+
ConversationHeaderProps,
|
|
73
|
+
ConversationHeaderLabels,
|
|
74
|
+
ConversationHeaderClassNames,
|
|
75
|
+
} from './ConversationHeader'
|
|
76
|
+
export { ConversationContextPanel, DEFAULT_CONVERSATION_CONTEXT_LABELS } from './ConversationContextPanel'
|
|
77
|
+
export type {
|
|
78
|
+
ConversationContextPanelProps,
|
|
79
|
+
ConversationContextEntry,
|
|
80
|
+
ConversationContextPanelLabels,
|
|
81
|
+
ConversationContextPanelClassNames,
|
|
82
|
+
} from './ConversationContextPanel'
|
|
83
|
+
export { WindowExpiredNotice, isWindowBlocking, DEFAULT_WINDOW_EXPIRED_LABELS } from './WindowExpiredNotice'
|
|
84
|
+
export type { WindowExpiredNoticeProps, WindowExpiredNoticeLabels } from './WindowExpiredNotice'
|
|
85
|
+
export { DocumentsLibrary, DEFAULT_DOCUMENTS_LIBRARY_LABELS } from './DocumentsLibrary'
|
|
86
|
+
export type { DocumentsLibraryProps, DocumentsLibraryLabels, DocumentsLibraryClassNames } from './DocumentsLibrary'
|
|
87
|
+
export { DOCUMENT_SOURCE_FILTER } from './ConversationDocumentsPanel'
|
|
88
|
+
export type { DocumentSourceFilter } from './ConversationDocumentsPanel'
|
|
89
|
+
export type { ConversationDocumentsPanelClassNames } from './ConversationDocumentsPanel'
|
|
90
|
+
export { ConversationDocumentsPanel, DEFAULT_CONVERSATION_DOCUMENTS_LABELS } from './ConversationDocumentsPanel'
|
|
91
|
+
export type { ConversationDocumentsPanelProps, ConversationDocumentsPanelLabels } from './ConversationDocumentsPanel'
|
|
92
|
+
export { buildTranscriptText, buildTranscriptFilename, downloadTextFile } from './conversationTranscript'
|
|
93
|
+
export type { BuildTranscriptTextParams } from './conversationTranscript'
|
|
94
|
+
|
|
95
|
+
export { useDarkMode, useIsDarkTheme } from './useDarkMode'
|
|
96
|
+
export { useIsNarrow, NARROW_MAX_WIDTH_PX } from './useIsNarrow'
|
|
23
97
|
export { useWaitingNotifications } from './useWaitingNotifications'
|
|
98
|
+
export type {
|
|
99
|
+
UseWaitingNotificationsLabels,
|
|
100
|
+
UseWaitingNotificationsParams,
|
|
101
|
+
UseWaitingNotificationsResult,
|
|
102
|
+
} from './useWaitingNotifications'
|
|
24
103
|
|
|
25
104
|
export { ConversationsProvider, useConversations } from './providers/ConversationsProvider'
|
|
26
105
|
|
|
@@ -29,6 +108,11 @@ export { ConversationsProvider, useConversations } from './providers/Conversatio
|
|
|
29
108
|
export { WhatsAppTemplateSettingsForm } from './settings/WhatsAppTemplateSettingsForm'
|
|
30
109
|
export { WhatsAppCreateTemplateForm } from './settings/WhatsAppCreateTemplateForm'
|
|
31
110
|
export { WelcomeFarewellForm } from './settings/WelcomeFarewellForm'
|
|
111
|
+
export {
|
|
112
|
+
WhatsAppTemplatesSettings,
|
|
113
|
+
TEMPLATE_SETTINGS_TAB,
|
|
114
|
+
DEFAULT_TEMPLATES_SETTINGS_LABELS,
|
|
115
|
+
} from './settings/WhatsAppTemplatesSettings'
|
|
32
116
|
export { TopicsForm } from './settings/TopicsForm'
|
|
33
117
|
|
|
34
118
|
// Camada headless (T6.9) — hooks de dados/ações independentes de qualquer tela, para o
|
|
@@ -38,27 +122,47 @@ export { useConversationList } from './hooks/useConversationList'
|
|
|
38
122
|
export { useConversationContext } from './hooks/useConversationContext'
|
|
39
123
|
export { useConversationDocuments } from './hooks/useConversationDocuments'
|
|
40
124
|
export { useConversationRealtime, useGlobalRealtime } from './hooks/useConversationRealtime'
|
|
125
|
+
export { useConversationActions, useInboxActions } from './hooks/useConversationActions'
|
|
41
126
|
|
|
42
127
|
export { parseWhatsAppFormatting, waToHTML, htmlToWA, waToHTMLInline } from './lib/whatsapp-formatting'
|
|
43
128
|
export { formatPhone, phoneInitials } from './lib/phone'
|
|
44
|
-
|
|
129
|
+
// `formatDateTime` e `isSameDay` já eram usados pelas bolhas e pelo divisor de data; exportá-los
|
|
130
|
+
// evita que cada host mantenha a própria cópia e acabe com timeline e transcript divergindo.
|
|
131
|
+
export { formatTimestamp, formatFileSize, formatDateTime, isSameDay } from './lib/format'
|
|
45
132
|
|
|
46
133
|
export type { MessagePayload, ConversationsUIConfig, ConversationsTheme, ConversationsFeatures } from './types'
|
|
47
|
-
export type {
|
|
134
|
+
export type { InteractivePayload, InteractiveSection, InteractiveOption, InteractiveSelection } from './types'
|
|
135
|
+
export type {
|
|
136
|
+
ConversationsApi,
|
|
137
|
+
SSEProvider,
|
|
138
|
+
ConversationEventSource,
|
|
139
|
+
ConversationSummary,
|
|
140
|
+
ConversationDocument,
|
|
141
|
+
ConversationPage,
|
|
142
|
+
ConversationDocumentPage,
|
|
143
|
+
CompanyDocument,
|
|
144
|
+
CompanyDocumentPage,
|
|
145
|
+
ConversationTemplate,
|
|
146
|
+
ListConversationsParams,
|
|
147
|
+
ListDocumentsParams,
|
|
148
|
+
} from './providers/types'
|
|
149
|
+
export type { UseConversationActionsResult, UseInboxActionsResult } from './hooks/useConversationActions'
|
|
48
150
|
|
|
49
151
|
export type { MessageBubbleProps } from './MessageBubble'
|
|
50
152
|
export type { ConversationWallpaperProps } from './Wallpaper'
|
|
51
153
|
export type { ConversationLocales, ConversationLocalesProviderProps } from './ConversationLocalesProvider'
|
|
52
154
|
export type { AudioPlayerProps } from './AudioPlayer'
|
|
53
|
-
export type { EmojiPickerProps } from './EmojiPicker'
|
|
54
|
-
export type {
|
|
55
|
-
export type {
|
|
155
|
+
export type { EmojiPickerProps, EmojiPickerLabels } from './EmojiPicker'
|
|
156
|
+
export type { EmojiEntry, EmojiCategory } from './emojiCatalog'
|
|
157
|
+
export type { InteractiveMessageProps, InteractiveMessageLabels } from './InteractiveMessage'
|
|
158
|
+
export type { MessageComposerProps, MessageComposerClassNames, MessageComposerLabels } from './MessageComposer'
|
|
159
|
+
export type { WhatsAppMessageEditorProps, WhatsAppMessageEditorLabels } from './WhatsAppMessageEditor'
|
|
56
160
|
export type { SimpleEmojiPickerProps } from './SimpleEmojiPicker'
|
|
57
|
-
export type { DateDividerProps } from './DateDivider'
|
|
58
|
-
export type { AvatarProps } from './Avatar'
|
|
59
|
-
export type { ConversationListItemProps } from './ConversationListItem'
|
|
161
|
+
export type { DateDividerProps, DateDividerClassNames } from './DateDivider'
|
|
162
|
+
export type { AvatarProps, AvatarLabels } from './Avatar'
|
|
163
|
+
export type { ConversationListItemProps, ConversationListItemLabels } from './ConversationListItem'
|
|
60
164
|
export type { StatusTicksProps } from './StatusTicks'
|
|
61
|
-
export type { LightboxProps } from './Lightbox'
|
|
165
|
+
export type { LightboxProps, LightboxLabels } from './Lightbox'
|
|
62
166
|
export type { MediaRendererProps, ResolveMediaUrl } from './MediaRenderer'
|
|
63
167
|
export type { FileIconProps } from './FileIcon'
|
|
64
168
|
export type { MessageTextProps } from './MessageText'
|
|
@@ -77,6 +181,11 @@ export type {
|
|
|
77
181
|
WhatsAppCreateTemplateFormLabels,
|
|
78
182
|
} from './settings/WhatsAppCreateTemplateForm'
|
|
79
183
|
export type { WelcomeFarewellFormProps, WelcomeFarewellFormLabels } from './settings/WelcomeFarewellForm'
|
|
184
|
+
export type {
|
|
185
|
+
WhatsAppTemplatesSettingsProps,
|
|
186
|
+
WhatsAppTemplatesSettingsLabels,
|
|
187
|
+
TemplateSettingsTab,
|
|
188
|
+
} from './settings/WhatsAppTemplatesSettings'
|
|
80
189
|
export type { TopicsFormProps, TopicItem, TopicsFormLabels } from './settings/TopicsForm'
|
|
81
190
|
|
|
82
191
|
export type { UseConversationMessagesResult } from './hooks/useConversationMessages'
|
|
@@ -85,3 +194,7 @@ export type { UseConversationContextResult } from './hooks/useConversationContex
|
|
|
85
194
|
export type { UseConversationDocumentsParams, UseConversationDocumentsResult } from './hooks/useConversationDocuments'
|
|
86
195
|
export type { ConversationRealtimeHandler } from './hooks/useConversationRealtime'
|
|
87
196
|
export type { AsyncResourceState } from './hooks/useAsyncResource'
|
|
197
|
+
export { createMediaUrlResolver } from './lib/createMediaUrlResolver'
|
|
198
|
+
export type { ConversationHeaderUtility } from './ConversationHeader'
|
|
199
|
+
export { applyQuickReplyVariables, resolveQuickReply } from './MessageComposer'
|
|
200
|
+
export type { QuickReply } from './MessageComposer'
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test'
|
|
2
|
+
|
|
3
|
+
import { cn } from './cn'
|
|
4
|
+
|
|
5
|
+
describe('cn', () => {
|
|
6
|
+
// O contrato de `classNames`/`className` dos componentes depende disto: se o override não vencer,
|
|
7
|
+
// a API de customização é decorativa. Concatenar falharia justo no caso comum — o Tailwind emite
|
|
8
|
+
// `px-2` antes de `px-4`, então a base ganharia de quem quer apertar o espaçamento.
|
|
9
|
+
it('o override do host vence a classe base em conflito', () => {
|
|
10
|
+
expect(cn('px-4 py-3', 'px-2')).toBe('py-3 px-2')
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('mantém o que não conflita', () => {
|
|
14
|
+
expect(cn('flex items-center gap-3', 'gap-1')).toBe('flex items-center gap-1')
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('preserva classes próprias do pacote, que o merge não conhece', () => {
|
|
18
|
+
expect(cn('cv-row flex', 'bg-red-50')).toBe('cv-row flex bg-red-50')
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('ignora ausente, falso e vazio', () => {
|
|
22
|
+
expect(cn('border-b', undefined, false, '')).toBe('border-b')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('aplica condicional na ordem recebida', () => {
|
|
26
|
+
const isCompact = false
|
|
27
|
+
expect(cn('mt-2', isCompact && 'mt-8', 'mt-4')).toBe('mt-4')
|
|
28
|
+
})
|
|
29
|
+
})
|
package/src/lib/cn.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Junta classes resolvendo conflitos de utilitário do Tailwind.
|
|
3
|
+
*
|
|
4
|
+
* Concatenar não basta: a ordem no atributo `class` não decide nada, quem decide é a ordem no CSS
|
|
5
|
+
* gerado. Com `px-4` na base e `px-2` vindo do host, o Tailwind emite `px-2` antes de `px-4` e a
|
|
6
|
+
* base ganharia — justo o caso de quem quer apertar o espaçamento. O `twMerge` descarta a classe
|
|
7
|
+
* base quando o host manda uma da mesma família, então o override do produto sempre vence.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { clsx, type ClassValue } from 'clsx'
|
|
11
|
+
import { twMerge } from 'tailwind-merge'
|
|
12
|
+
|
|
13
|
+
export function cn(...inputs: ClassValue[]): string {
|
|
14
|
+
return twMerge(clsx(inputs))
|
|
15
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve a mídia de uma mensagem numa URL exibível, usando só o `ConversationsApi`.
|
|
3
|
+
*
|
|
4
|
+
* Mora no pacote, e não em cada host, porque a regra não tem nada de específico de produto: é a
|
|
5
|
+
* tradução de `uploadId`/`mediaId` pelos dois métodos que o próprio contrato já declara. Deixá-la no
|
|
6
|
+
* host significava que todo projeto que adotasse o SDK reescreveria as mesmas oito linhas — e, na
|
|
7
|
+
* prática, ninguém escrevia: o `MediaRenderer` só busca mídia pela porta `onResolveMediaUrl`, então
|
|
8
|
+
* onde nada era injetado foto, vídeo e áudio ficavam no placeholder para sempre.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { MessagePayload } from '../types'
|
|
12
|
+
import type { ConversationsApi } from '../providers/types'
|
|
13
|
+
import type { ResolveMediaUrl } from '../MediaRenderer'
|
|
14
|
+
|
|
15
|
+
export function createMediaUrlResolver(
|
|
16
|
+
api: Pick<ConversationsApi, 'getDocumentUrl' | 'getMediaProxyUrl'>,
|
|
17
|
+
): ResolveMediaUrl {
|
|
18
|
+
return async (message: MessagePayload): Promise<string | null> => {
|
|
19
|
+
// Mídia já copiada para o storage do host: sai por URL assinada e o binário não passa pela API.
|
|
20
|
+
// `inline` porque aqui o arquivo é para VER na tela — `attachment` faria o navegador baixar.
|
|
21
|
+
if (message.uploadId) return api.getDocumentUrl(message.uploadId, 'inline')
|
|
22
|
+
|
|
23
|
+
// Antes da ingestão só existe o id na Meta, cuja URL expira; o backend busca e devolve base64.
|
|
24
|
+
// Data URL serve de `src` para `<img>`/`<video>`/`<audio>`: o bloqueio do Chrome a `data:` vale
|
|
25
|
+
// para navegação de topo, não para carregar mídia dentro da página.
|
|
26
|
+
if (message.mediaId) {
|
|
27
|
+
const { mimeType, data } = await api.getMediaProxyUrl(message.mediaId)
|
|
28
|
+
return `data:${mimeType};base64,${data}`
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return null
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test'
|
|
2
|
+
import { conversationsOf, documentsOf, totalOf } from './paginated'
|
|
3
|
+
import type { ConversationDocument, ConversationSummary } from '../providers/types'
|
|
4
|
+
|
|
5
|
+
const conversation = { id: '1', whatsappNumber: '5511900000000' } as ConversationSummary
|
|
6
|
+
const document = { id: 'd1', filename: 'contrato.pdf' } as ConversationDocument
|
|
7
|
+
|
|
8
|
+
describe('normalização de lista ou página', () => {
|
|
9
|
+
// As duas formas existem porque o contrato aceita as duas: array puro é o retorno original, e
|
|
10
|
+
// implementações antigas continuam válidas. Um consumidor que só tratasse uma delas quebraria
|
|
11
|
+
// em runtime, não no compilador.
|
|
12
|
+
it('aceita o array puro do contrato original', () => {
|
|
13
|
+
expect(conversationsOf([conversation])).toEqual([conversation])
|
|
14
|
+
expect(documentsOf([document])).toEqual([document])
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('desembrulha a forma paginada', () => {
|
|
18
|
+
expect(conversationsOf({ conversations: [conversation], total: 42 })).toEqual([conversation])
|
|
19
|
+
expect(documentsOf({ documents: [document], total: 7 })).toEqual([document])
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('usa o total do servidor quando ele vem, e o tamanho da página quando não vem', () => {
|
|
23
|
+
// A distinção importa: com array puro não dá para saber se a página é a última, então o
|
|
24
|
+
// melhor palpite honesto é o que se tem em mãos.
|
|
25
|
+
expect(totalOf({ conversations: [conversation], total: 42 })).toBe(42)
|
|
26
|
+
expect(totalOf([conversation])).toBe(1)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('trata página vazia sem confundir com ausência de dados', () => {
|
|
30
|
+
expect(conversationsOf({ conversations: [], total: 0 })).toEqual([])
|
|
31
|
+
expect(totalOf({ conversations: [], total: 0 })).toBe(0)
|
|
32
|
+
})
|
|
33
|
+
})
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ConversationDocument,
|
|
3
|
+
ConversationDocumentPage,
|
|
4
|
+
ConversationPage,
|
|
5
|
+
ConversationSummary,
|
|
6
|
+
} from '../providers/types'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* `fetchConversations` e `getDocuments` devolvem o array puro (contrato original) ou uma página
|
|
10
|
+
* com total. Normalizar num lugar só evita que cada consumidor invente sua própria checagem —
|
|
11
|
+
* e um consumidor esquecido não falha no compilador, falha em runtime com `.map is not a
|
|
12
|
+
* function`.
|
|
13
|
+
*/
|
|
14
|
+
export function conversationsOf(result: ConversationSummary[] | ConversationPage): ConversationSummary[] {
|
|
15
|
+
return Array.isArray(result) ? result : result.conversations
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function documentsOf(result: ConversationDocument[] | ConversationDocumentPage): ConversationDocument[] {
|
|
19
|
+
return Array.isArray(result) ? result : result.documents
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function totalOf(
|
|
23
|
+
result: ConversationSummary[] | ConversationPage | ConversationDocument[] | ConversationDocumentPage,
|
|
24
|
+
): number {
|
|
25
|
+
return Array.isArray(result) ? result.length : result.total
|
|
26
|
+
}
|