@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.
Files changed (99) hide show
  1. package/dist/{chunk-ZDURDZTM.js → chunk-2AYDBWNE.js} +14 -0
  2. package/dist/chunk-OIDAIVCH.js +1985 -0
  3. package/dist/flows/index.d.ts +14 -2
  4. package/dist/flows/index.js +117 -39
  5. package/dist/index.d.ts +729 -136
  6. package/dist/index.js +1418 -1104
  7. package/dist/preview/index.d.ts +373 -0
  8. package/dist/preview/index.js +1247 -0
  9. package/dist/styles.css +198 -0
  10. package/dist/types-O7kMP1Yn.d.ts +416 -0
  11. package/package.json +10 -3
  12. package/src/AudioRecorderButton.test.tsx +30 -0
  13. package/src/AudioRecorderButton.tsx +248 -0
  14. package/src/Avatar.tsx +30 -4
  15. package/src/ChannelIcon.tsx +87 -0
  16. package/src/ConversationContextPanel.tsx +127 -0
  17. package/src/ConversationDocumentsPanel.tsx +425 -0
  18. package/src/ConversationHeader.tsx +257 -0
  19. package/src/ConversationListItem.tsx +54 -7
  20. package/src/ConversationLocalesProvider.tsx +16 -0
  21. package/src/ConversationRow.tsx +137 -0
  22. package/src/DateDivider.tsx +16 -3
  23. package/src/DocumentsLibrary.tsx +322 -0
  24. package/src/EmojiPicker.tsx +69 -55
  25. package/src/FileIcon.test.ts +83 -0
  26. package/src/FileIcon.tsx +88 -11
  27. package/src/InteractiveMessage.test.tsx +41 -0
  28. package/src/InteractiveMessage.tsx +143 -0
  29. package/src/Lightbox.tsx +18 -3
  30. package/src/MediaRenderer.tsx +64 -21
  31. package/src/MessageBubble.tsx +54 -5
  32. package/src/MessageComposer.test.tsx +35 -0
  33. package/src/MessageComposer.tsx +146 -19
  34. package/src/RichMessageComposer.test.tsx +83 -0
  35. package/src/RichMessageComposer.tsx +380 -0
  36. package/src/Wallpaper.test.tsx +21 -0
  37. package/src/Wallpaper.tsx +54 -6
  38. package/src/WhatsAppMessageEditor.tsx +28 -4
  39. package/src/WindowExpiredNotice.tsx +57 -0
  40. package/src/audioRecorderFormat.test.ts +67 -0
  41. package/src/conversationChannel.test.ts +53 -0
  42. package/src/conversationChannel.ts +146 -0
  43. package/src/conversationTranscript.test.ts +65 -0
  44. package/src/conversationTranscript.ts +64 -0
  45. package/src/conversationWindow.test.ts +90 -0
  46. package/src/conversationWindow.ts +78 -0
  47. package/src/documentTypeLabel.test.ts +57 -0
  48. package/src/emojiCatalog.test.ts +35 -0
  49. package/src/emojiCatalog.ts +189 -0
  50. package/src/flows/FlowGroupHeader.tsx +12 -2
  51. package/src/flows/FlowMapCanvas.tsx +17 -14
  52. package/src/flows/FlowMapNode.tsx +3 -1
  53. package/src/flows/FlowNodeCard.tsx +22 -4
  54. package/src/flows/FlowNodePanel.tsx +132 -35
  55. package/src/flows/FlowPalette.tsx +6 -2
  56. package/src/flows/FlowWhatsAppPreview.tsx +14 -3
  57. package/src/flows/flowGraph.ts +5 -5
  58. package/src/flows/labels.ts +5 -0
  59. package/src/hooks/useConversationActions.ts +56 -0
  60. package/src/hooks/useConversationDocuments.ts +15 -9
  61. package/src/hooks/useConversationList.ts +15 -9
  62. package/src/hooks/useConversationMessages.ts +2 -2
  63. package/src/index.ts +129 -16
  64. package/src/lib/cn.test.ts +29 -0
  65. package/src/lib/cn.ts +15 -0
  66. package/src/lib/createMediaUrlResolver.ts +33 -0
  67. package/src/lib/paginated.test.ts +33 -0
  68. package/src/lib/paginated.ts +26 -0
  69. package/src/lib/phone.ts +34 -0
  70. package/src/preview/ConversationPreview.tsx +325 -0
  71. package/src/preview/MediaTypesPreview.tsx +87 -0
  72. package/src/preview/conversationPreviewFailures.test.ts +64 -0
  73. package/src/preview/createMockConversationsApi.ts +271 -0
  74. package/src/preview/createMockSSEProvider.ts +40 -0
  75. package/src/preview/createPreviewBridgeClient.test.ts +92 -0
  76. package/src/preview/createPreviewBridgeClient.ts +88 -0
  77. package/src/preview/createPreviewWebhookClient.test.ts +105 -0
  78. package/src/preview/createPreviewWebhookClient.ts +131 -0
  79. package/src/preview/index.ts +61 -0
  80. package/src/preview/mediaTypeOf.test.ts +15 -0
  81. package/src/preview/mockDocumentsSearch.test.ts +57 -0
  82. package/src/preview/mockEventSource.ts +53 -0
  83. package/src/preview/preview.test.ts +177 -0
  84. package/src/preview/previewFileSamples.test.ts +151 -0
  85. package/src/preview/previewFileSamples.ts +74 -0
  86. package/src/preview/previewFixtures.ts +440 -0
  87. package/src/preview/previewMediaSource.test.ts +62 -0
  88. package/src/preview/previewMediaSource.ts +91 -0
  89. package/src/preview/previewStore.ts +193 -0
  90. package/src/preview/startPreviewScript.ts +60 -0
  91. package/src/providers/types.ts +163 -11
  92. package/src/quickReply.test.ts +58 -0
  93. package/src/settings/WhatsAppCreateTemplateForm.tsx +3 -1
  94. package/src/settings/WhatsAppTemplatesSettings.tsx +106 -0
  95. package/src/styles.css +136 -0
  96. package/src/types.ts +46 -1
  97. package/src/useDarkMode.ts +26 -0
  98. package/src/useIsNarrow.ts +29 -0
  99. 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"><MessageCircleQuestion size={15} className="text-blue-500" /> {labels.palette.question}</span>
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"><Zap size={15} className="text-orange-500" /> {labels.palette.action}</span>
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({ body, options, labels = DEFAULT_FLOW_EDITOR_LABELS.nodePanel }: FlowWhatsAppPreviewProps) {
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 ? parseWhatsAppFormatting(body) : <span className="italic text-gray-400">{labels.previewEmptyBody}</span>}
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 key={id} 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">
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
  ))}
@@ -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
- export const BUILT_IN_ACTION_KINDS = {
32
- HANDOFF: 'handoff',
33
- RATE_LIMITED_HANDOFF: 'rate_limited_handoff',
34
- SEND_PRODUCT_LIST: 'send_product_list',
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
@@ -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 type { ConversationDocument } from '../providers/types'
3
+ import { documentsOf, totalOf } from '../lib/paginated'
4
+ import type { ConversationDocument, ListDocumentsParams } from '../providers/types'
4
5
 
5
- export interface UseConversationDocumentsParams {
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
- return { documents: data ?? [], loading, error, refetch }
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 type { ConversationSummary } from '../providers/types'
3
+ import { conversationsOf, totalOf } from '../lib/paginated'
4
+ import type { ConversationSummary, ListConversationsParams } from '../providers/types'
4
5
 
5
- export interface UseConversationListParams {
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
- return { conversations: data ?? [], loading, error, refetch }
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: string; languageCode?: string; bodyParams?: string[] }) => Promise<void>
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: string; languageCode?: string; bodyParams?: string[] }) => {
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 { MessageComposer } from './MessageComposer'
7
- export { WhatsAppMessageEditor } from './WhatsAppMessageEditor'
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
- export { useDarkMode } from './useDarkMode'
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
- export { formatTimestamp, formatFileSize } from './lib/format'
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 { ConversationsApi, SSEProvider, ConversationSummary, ConversationDocument } from './providers/types'
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 { MessageComposerProps } from './MessageComposer'
55
- export type { WhatsAppMessageEditorProps } from './WhatsAppMessageEditor'
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
+ }