@adatechnology/conversations-ui 0.1.0-rc.2 → 0.1.0-rc.21

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 (106) hide show
  1. package/dist/{chunk-ZDURDZTM.js → chunk-2AYDBWNE.js} +14 -0
  2. package/dist/chunk-TV4OQRGH.js +2187 -0
  3. package/dist/flows/index.d.ts +14 -2
  4. package/dist/flows/index.js +117 -39
  5. package/dist/index.d.ts +862 -136
  6. package/dist/index.js +1746 -1124
  7. package/dist/preview/index.d.ts +470 -0
  8. package/dist/preview/index.js +1329 -0
  9. package/dist/styles.css +228 -0
  10. package/dist/types-C6A_9edv.d.ts +456 -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/AudioTranscription.test.tsx +115 -0
  15. package/src/AudioTranscription.tsx +249 -0
  16. package/src/Avatar.tsx +30 -4
  17. package/src/ChannelIcon.tsx +87 -0
  18. package/src/ConversationContextPanel.tsx +280 -0
  19. package/src/ConversationDocumentsPanel.tsx +425 -0
  20. package/src/ConversationHeader.tsx +257 -0
  21. package/src/ConversationListItem.tsx +54 -7
  22. package/src/ConversationLocalesProvider.tsx +44 -0
  23. package/src/ConversationRow.tsx +137 -0
  24. package/src/DateDivider.tsx +16 -3
  25. package/src/DocumentsLibrary.tsx +322 -0
  26. package/src/EmojiPicker.tsx +69 -55
  27. package/src/FileIcon.test.ts +83 -0
  28. package/src/FileIcon.tsx +88 -11
  29. package/src/InteractiveMessage.test.tsx +41 -0
  30. package/src/InteractiveMessage.tsx +143 -0
  31. package/src/Lightbox.tsx +18 -3
  32. package/src/MediaRenderer.tsx +96 -22
  33. package/src/MessageBubble.tsx +77 -5
  34. package/src/MessageComposer.test.tsx +35 -0
  35. package/src/MessageComposer.tsx +165 -19
  36. package/src/RichMessageComposer.test.tsx +83 -0
  37. package/src/RichMessageComposer.tsx +380 -0
  38. package/src/Wallpaper.test.tsx +21 -0
  39. package/src/Wallpaper.tsx +69 -7
  40. package/src/WhatsAppMessageEditor.tsx +28 -4
  41. package/src/WindowExpiredNotice.tsx +57 -0
  42. package/src/audioRecorderFormat.test.ts +67 -0
  43. package/src/conversationChannel.test.ts +53 -0
  44. package/src/conversationChannel.ts +146 -0
  45. package/src/conversationTranscript.test.ts +122 -0
  46. package/src/conversationTranscript.ts +89 -0
  47. package/src/conversationWindow.test.ts +90 -0
  48. package/src/conversationWindow.ts +78 -0
  49. package/src/documentTypeLabel.test.ts +57 -0
  50. package/src/emojiCatalog.test.ts +35 -0
  51. package/src/emojiCatalog.ts +189 -0
  52. package/src/flows/FlowGroupHeader.tsx +12 -2
  53. package/src/flows/FlowMapCanvas.tsx +17 -14
  54. package/src/flows/FlowMapNode.tsx +3 -1
  55. package/src/flows/FlowNodeCard.tsx +22 -4
  56. package/src/flows/FlowNodePanel.tsx +132 -35
  57. package/src/flows/FlowPalette.tsx +6 -2
  58. package/src/flows/FlowWhatsAppPreview.tsx +14 -3
  59. package/src/flows/flowGraph.ts +5 -5
  60. package/src/flows/labels.ts +5 -0
  61. package/src/hooks/useConversationActions.ts +56 -0
  62. package/src/hooks/useConversationDocuments.ts +15 -9
  63. package/src/hooks/useConversationList.ts +15 -9
  64. package/src/hooks/useConversationMessages.ts +2 -2
  65. package/src/hooks/useScrollToLatestMessage.ts +127 -0
  66. package/src/index.ts +140 -16
  67. package/src/lib/cn.test.ts +29 -0
  68. package/src/lib/cn.ts +15 -0
  69. package/src/lib/createMediaUrlResolver.ts +33 -0
  70. package/src/lib/paginated.test.ts +33 -0
  71. package/src/lib/paginated.ts +26 -0
  72. package/src/lib/phone.ts +34 -0
  73. package/src/preview/ConversationPreview.tsx +334 -0
  74. package/src/preview/MediaTypesPreview.tsx +87 -0
  75. package/src/preview/conversationPreviewFailures.test.ts +64 -0
  76. package/src/preview/createMockConversationsApi.ts +271 -0
  77. package/src/preview/createMockSSEProvider.ts +40 -0
  78. package/src/preview/createPreviewBridgeClient.test.ts +92 -0
  79. package/src/preview/createPreviewBridgeClient.ts +124 -0
  80. package/src/preview/createPreviewMediaUploader.ts +82 -0
  81. package/src/preview/createPreviewWebhookClient.test.ts +194 -0
  82. package/src/preview/createPreviewWebhookClient.ts +222 -0
  83. package/src/preview/index.ts +67 -0
  84. package/src/preview/mediaTypeOf.test.ts +15 -0
  85. package/src/preview/mockDocumentsSearch.test.ts +57 -0
  86. package/src/preview/mockEventSource.ts +53 -0
  87. package/src/preview/preview.test.ts +177 -0
  88. package/src/preview/previewFileSamples.test.ts +151 -0
  89. package/src/preview/previewFileSamples.ts +74 -0
  90. package/src/preview/previewFixtures.ts +440 -0
  91. package/src/preview/previewMediaSource.test.ts +62 -0
  92. package/src/preview/previewMediaSource.ts +91 -0
  93. package/src/preview/previewMediaUploader.test.ts +61 -0
  94. package/src/preview/previewStore.ts +193 -0
  95. package/src/preview/startPreviewScript.ts +60 -0
  96. package/src/providers/types.ts +175 -12
  97. package/src/quickReply.test.ts +58 -0
  98. package/src/settings/TranscriptionSettingsForm.test.tsx +81 -0
  99. package/src/settings/TranscriptionSettingsForm.tsx +189 -0
  100. package/src/settings/WhatsAppCreateTemplateForm.tsx +3 -1
  101. package/src/settings/WhatsAppTemplatesSettings.tsx +106 -0
  102. package/src/styles.css +173 -0
  103. package/src/types.ts +72 -1
  104. package/src/useDarkMode.ts +26 -0
  105. package/src/useIsNarrow.ts +29 -0
  106. package/src/useWaitingNotifications.ts +74 -29
@@ -0,0 +1,127 @@
1
+ import { useCallback, useLayoutEffect, useRef, useState, type RefObject, type UIEvent } from 'react'
2
+
3
+ /**
4
+ * Mantém a conversa no fim, como todo mensageiro.
5
+ *
6
+ * Abrir uma conversa no topo do histórico é errado por um motivo simples: o que interessa ao
7
+ * atendente é a última mensagem, e ele teria que rolar por semanas de conversa para chegar nela.
8
+ *
9
+ * Existe no pacote, e não em cada host, porque a regra tem duas sutilezas que se descobre só
10
+ * errando — o salto instantâneo na troca de conversa e o respeito a quem rolou para trás — e
11
+ * reimplementá-las em cada inbox garante que uma delas fique de fora.
12
+ */
13
+
14
+ /**
15
+ * Distância do fim, em pixels, dentro da qual ainda consideramos o operador "acompanhando".
16
+ *
17
+ * Não é zero porque o navegador arredonda `scrollTop` fracionário em telas com zoom ou densidade
18
+ * alta: exigir o fim exato faria o painel achar que o operador rolou para trás sem ele ter tocado
19
+ * em nada, e a próxima mensagem não apareceria.
20
+ */
21
+ const NEAR_BOTTOM_THRESHOLD_PX = 120
22
+
23
+ /**
24
+ * Toda rolagem aqui é `'auto'` — nunca `'smooth'`.
25
+ *
26
+ * Tentamos suave para mensagem nova e medimos: em ambiente onde a rolagem suave está desligada, o
27
+ * `scrollTo({ behavior: 'smooth' })` **não faz nada e não avisa** — a mensagem nova simplesmente não
28
+ * entra na vista. E não dá para detectar isso pelo `prefers-reduced-motion`: no navegador em que
29
+ * reproduzimos, a media query respondia `false` e o smooth continuava sendo no-op.
30
+ *
31
+ * Trocar uma animação cosmética por garantia de que o operador vê a mensagem é barato: seguir uma
32
+ * mensagem nova salta a altura de uma bolha, que é quase imperceptível de qualquer forma.
33
+ */
34
+ const SCROLL_BEHAVIOR: ScrollBehavior = 'auto'
35
+
36
+ export type UseScrollToLatestMessageParams = {
37
+ /** Troca de conversa. Muda ⇒ salto instantâneo para o fim. */
38
+ readonly conversationId: string | undefined
39
+ /** Quantidade de mensagens carregadas. Cresce ⇒ acompanha o fim, se o operador estiver lá. */
40
+ readonly messageCount: number
41
+ }
42
+
43
+ export type UseScrollToLatestMessageResult = {
44
+ /** Vai no elemento que rola — tipicamente o `ConversationWallpaper`. */
45
+ readonly containerRef: RefObject<HTMLDivElement | null>
46
+ /** Ligue no `onScroll` do mesmo elemento: é o que detecta o operador lendo o histórico. */
47
+ readonly handleScroll: (event: UIEvent<HTMLDivElement>) => void
48
+ /** `true` quando o operador rolou para trás — serve a um botão "ir para a última". */
49
+ readonly isAwayFromBottom: boolean
50
+ readonly scrollToBottom: (behavior?: ScrollBehavior) => void
51
+ }
52
+
53
+ export function useScrollToLatestMessage({
54
+ conversationId,
55
+ messageCount,
56
+ }: UseScrollToLatestMessageParams): UseScrollToLatestMessageResult {
57
+ const containerRef = useRef<HTMLDivElement | null>(null)
58
+ const [isAwayFromBottom, setIsAwayFromBottom] = useState(false)
59
+
60
+ /**
61
+ * Espelha `isAwayFromBottom` para o efeito de mensagens novas ler o valor atual sem depender dele.
62
+ *
63
+ * Se o efeito dependesse do estado, cada rolagem do operador o re-disparava e o puxava de volta
64
+ * para o fim — exatamente o que a checagem existe para evitar.
65
+ */
66
+ const isAwayFromBottomRef = useRef(false)
67
+
68
+ const scrollToBottom = useCallback((behavior: ScrollBehavior = SCROLL_BEHAVIOR) => {
69
+ const container = containerRef.current
70
+ if (!container) return
71
+ container.scrollTo({ top: container.scrollHeight, behavior })
72
+ }, [])
73
+
74
+ const handleScroll = useCallback((event: UIEvent<HTMLDivElement>) => {
75
+ const target = event.currentTarget
76
+ const distanceFromBottom = target.scrollHeight - target.scrollTop - target.clientHeight
77
+ const away = distanceFromBottom > NEAR_BOTTOM_THRESHOLD_PX
78
+
79
+ isAwayFromBottomRef.current = away
80
+ setIsAwayFromBottom((current) => (current === away ? current : away))
81
+ }, [])
82
+
83
+ /**
84
+ * Qual conversa já recebeu o salto de abertura.
85
+ *
86
+ * É o que separa "abriu a conversa" de "chegou mensagem", e não dá para usar só `conversationId`
87
+ * num efeito próprio: quando a conversa troca, a lista de mensagens ainda está vazia, então um
88
+ * salto ali rola um container sem conteúdo e não faz nada. O salto real precisa esperar a primeira
89
+ * leva de mensagens — e foi exatamente isso que deixou a conversa abrindo no topo.
90
+ */
91
+ const jumpedForConversationRef = useRef<string | undefined>(undefined)
92
+
93
+ useLayoutEffect(() => {
94
+ const isNewConversation = jumpedForConversationRef.current !== conversationId
95
+
96
+ if (isNewConversation) {
97
+ // Zera antes de qualquer coisa: "rolado para trás" da conversa anterior não pode bloquear o
98
+ // salto de abertura desta.
99
+ isAwayFromBottomRef.current = false
100
+ setIsAwayFromBottom(false)
101
+
102
+ // Sem mensagens ainda — o salto acontece quando a primeira leva chegar.
103
+ if (messageCount === 0) return
104
+
105
+ jumpedForConversationRef.current = conversationId
106
+ /**
107
+ * `'auto'`, sempre. Animar a rolagem por meses de histórico demora, mostra um borrão de
108
+ * mensagens antigas que ninguém pediu, e some por completo onde a rolagem suave está desligada
109
+ * (`prefers-reduced-motion`, alguns navegadores automatizados) — a conversa simplesmente
110
+ * abriria no topo. Abertura é salto, não animação.
111
+ */
112
+ scrollToBottom(SCROLL_BEHAVIOR)
113
+ return
114
+ }
115
+
116
+ /**
117
+ * Mensagem nova: acompanha, mas só se o operador já estava no fim.
118
+ *
119
+ * Puxar a rolagem de quem está lendo o histórico é pior do que não mostrar a mensagem — ele perde
120
+ * a posição e não sabe por quê. Quem rolou para trás recebe `isAwayFromBottom` e decide.
121
+ */
122
+ if (isAwayFromBottomRef.current) return
123
+ scrollToBottom(SCROLL_BEHAVIOR)
124
+ }, [conversationId, messageCount, scrollToBottom])
125
+
126
+ return { containerRef, handleScroll, isAwayFromBottom, scrollToBottom }
127
+ }
package/src/index.ts CHANGED
@@ -1,26 +1,107 @@
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 { AudioTranscription } from './AudioTranscription'
7
+ export { EmojiPicker, DEFAULT_EMOJI_PICKER_LABELS } from './EmojiPicker'
8
+ export { EMOJI_CATEGORIES, searchEmojis } from './emojiCatalog'
9
+ export { MessageComposer, DEFAULT_MESSAGE_COMPOSER_LABELS, DEFAULT_ACCEPTED_FILE_TYPES } from './MessageComposer'
10
+ export { RichMessageComposer, RICH_COMPOSER_ACTION, DEFAULT_RICH_COMPOSER_TOOLTIPS } from './RichMessageComposer'
11
+ export type {
12
+ RichMessageComposerProps,
13
+ RichComposerAction,
14
+ RichComposerTooltips,
15
+ RichComposerQuickReply,
16
+ RichComposerVariable,
17
+ RichMessageComposerHandle,
18
+ } from './RichMessageComposer'
19
+ export {
20
+ AudioRecorderButton,
21
+ DEFAULT_AUDIO_RECORDER_BUTTON_LABELS,
22
+ DEFAULT_MAX_RECORDING_MILLISECONDS,
23
+ } from './AudioRecorderButton'
24
+ export type { AudioRecorderButtonProps, AudioRecorderButtonLabels } from './AudioRecorderButton'
25
+ export { WhatsAppMessageEditor, DEFAULT_WHATSAPP_MESSAGE_EDITOR_LABELS } from './WhatsAppMessageEditor'
8
26
  export { SimpleEmojiPicker } from './SimpleEmojiPicker'
9
27
  export { DateDivider } from './DateDivider'
10
- export { Avatar } from './Avatar'
11
- export { ConversationListItem } from './ConversationListItem'
28
+ export { Avatar, DEFAULT_AVATAR_LABELS } from './Avatar'
29
+ export { ConversationListItem, DEFAULT_CONVERSATION_LIST_ITEM_LABELS } from './ConversationListItem'
12
30
  export { ToastProvider, useToast, toast } from './Toast'
13
31
 
14
32
  export { StatusTicks } from './StatusTicks'
15
- export { Lightbox } from './Lightbox'
33
+ export { Lightbox, DEFAULT_LIGHTBOX_LABELS } from './Lightbox'
16
34
  export { MediaRenderer } from './MediaRenderer'
17
35
  export { FileIcon } from './FileIcon'
18
36
  export { MessageText } from './MessageText'
19
37
  export { MessageTimestamp } from './MessageTimestamp'
20
38
  export { MessageTail } from './MessageTail'
21
39
 
22
- export { useDarkMode } from './useDarkMode'
40
+ // Operação da inbox: janela de 24h da Meta, linha com ações e painéis do atendimento. Regras da
41
+ // plataforma e do ofício de atender, não de um produto — por isso moram aqui.
42
+ export { CONVERSATION_WINDOW, WINDOW_FILTERS, windowOf, formatStalledFor } from './conversationWindow'
43
+ export type { WindowOfParams } from './conversationWindow'
44
+ // Canal de origem: capacidades por plataforma (janela de sessão, reabertura, tipo de identificador).
45
+ export {
46
+ CONVERSATION_CHANNEL,
47
+ DEFAULT_CONVERSATION_CHANNEL,
48
+ CHANNEL_CAPABILITIES,
49
+ channelFiltersFor,
50
+ CHANNEL_FILTER_ALL,
51
+ REOPEN_MECHANISM,
52
+ HANDLE_KIND,
53
+ capabilitiesOf,
54
+ formatContactHandle,
55
+ contactFlag,
56
+ } from './conversationChannel'
57
+ export type {
58
+ ConversationChannel,
59
+ ChannelCapabilities,
60
+ ChannelFilter,
61
+ ChannelFilterOption,
62
+ ReopenMechanism,
63
+ HandleKind,
64
+ FormatContactHandleParams,
65
+ } from './conversationChannel'
66
+ export type { ConversationWindow } from './conversationWindow'
67
+ export { ConversationRow } from './ConversationRow'
68
+ export { ChannelIcon, CHANNEL_BRAND_COLOR } from './ChannelIcon'
69
+ export type { ChannelIconProps } from './ChannelIcon'
70
+ export type { ConversationRowProps, ConversationRowClassNames } from './ConversationRow'
71
+ export { ConversationHeader, DEFAULT_CONVERSATION_HEADER_LABELS } from './ConversationHeader'
72
+ export type {
73
+ ConversationHeaderProps,
74
+ ConversationHeaderLabels,
75
+ ConversationHeaderClassNames,
76
+ } from './ConversationHeader'
77
+ export { ConversationContextPanel, DEFAULT_CONVERSATION_CONTEXT_LABELS } from './ConversationContextPanel'
78
+ export type {
79
+ ConversationContextPanelProps,
80
+ ConversationContextEntry,
81
+ ConversationContextStatus,
82
+ ConversationContextPanelLabels,
83
+ ConversationContextPanelClassNames,
84
+ } from './ConversationContextPanel'
85
+ export { WindowExpiredNotice, isWindowBlocking, DEFAULT_WINDOW_EXPIRED_LABELS } from './WindowExpiredNotice'
86
+ export type { WindowExpiredNoticeProps, WindowExpiredNoticeLabels } from './WindowExpiredNotice'
87
+ export { DocumentsLibrary, DEFAULT_DOCUMENTS_LIBRARY_LABELS } from './DocumentsLibrary'
88
+ export type { DocumentsLibraryProps, DocumentsLibraryLabels, DocumentsLibraryClassNames } from './DocumentsLibrary'
89
+ export { DOCUMENT_SOURCE_FILTER } from './ConversationDocumentsPanel'
90
+ export type { DocumentSourceFilter } from './ConversationDocumentsPanel'
91
+ export type { ConversationDocumentsPanelClassNames } from './ConversationDocumentsPanel'
92
+ export { ConversationDocumentsPanel, DEFAULT_CONVERSATION_DOCUMENTS_LABELS } from './ConversationDocumentsPanel'
93
+ export type { ConversationDocumentsPanelProps, ConversationDocumentsPanelLabels } from './ConversationDocumentsPanel'
94
+ export { buildTranscriptText, buildTranscriptFilename, downloadTextFile } from './conversationTranscript'
95
+ export type { BuildTranscriptTextParams } from './conversationTranscript'
96
+
97
+ export { useDarkMode, useIsDarkTheme } from './useDarkMode'
98
+ export { useIsNarrow, NARROW_MAX_WIDTH_PX } from './useIsNarrow'
23
99
  export { useWaitingNotifications } from './useWaitingNotifications'
100
+ export type {
101
+ UseWaitingNotificationsLabels,
102
+ UseWaitingNotificationsParams,
103
+ UseWaitingNotificationsResult,
104
+ } from './useWaitingNotifications'
24
105
 
25
106
  export { ConversationsProvider, useConversations } from './providers/ConversationsProvider'
26
107
 
@@ -29,36 +110,65 @@ export { ConversationsProvider, useConversations } from './providers/Conversatio
29
110
  export { WhatsAppTemplateSettingsForm } from './settings/WhatsAppTemplateSettingsForm'
30
111
  export { WhatsAppCreateTemplateForm } from './settings/WhatsAppCreateTemplateForm'
31
112
  export { WelcomeFarewellForm } from './settings/WelcomeFarewellForm'
113
+ export { TranscriptionSettingsForm } from './settings/TranscriptionSettingsForm'
114
+ export {
115
+ WhatsAppTemplatesSettings,
116
+ TEMPLATE_SETTINGS_TAB,
117
+ DEFAULT_TEMPLATES_SETTINGS_LABELS,
118
+ } from './settings/WhatsAppTemplatesSettings'
32
119
  export { TopicsForm } from './settings/TopicsForm'
33
120
 
34
121
  // Camada headless (T6.9) — hooks de dados/ações independentes de qualquer tela, para o
35
122
  // produto montar sua própria UI sobre eles. Requerem <ConversationsProvider> como ancestral.
36
123
  export { useConversationMessages } from './hooks/useConversationMessages'
124
+ export { useScrollToLatestMessage } from './hooks/useScrollToLatestMessage'
37
125
  export { useConversationList } from './hooks/useConversationList'
38
126
  export { useConversationContext } from './hooks/useConversationContext'
39
127
  export { useConversationDocuments } from './hooks/useConversationDocuments'
40
128
  export { useConversationRealtime, useGlobalRealtime } from './hooks/useConversationRealtime'
129
+ export { useConversationActions, useInboxActions } from './hooks/useConversationActions'
41
130
 
42
131
  export { parseWhatsAppFormatting, waToHTML, htmlToWA, waToHTMLInline } from './lib/whatsapp-formatting'
43
132
  export { formatPhone, phoneInitials } from './lib/phone'
44
- export { formatTimestamp, formatFileSize } from './lib/format'
133
+ // `formatDateTime` e `isSameDay` já eram usados pelas bolhas e pelo divisor de data; exportá-los
134
+ // evita que cada host mantenha a própria cópia e acabe com timeline e transcript divergindo.
135
+ export { formatTimestamp, formatFileSize, formatDateTime, isSameDay } from './lib/format'
45
136
 
46
137
  export type { MessagePayload, ConversationsUIConfig, ConversationsTheme, ConversationsFeatures } from './types'
47
- export type { ConversationsApi, SSEProvider, ConversationSummary, ConversationDocument } from './providers/types'
138
+ export type { InteractivePayload, InteractiveSection, InteractiveOption, InteractiveSelection } from './types'
139
+ export type { MessageTranscription, TranscriptionStatus, TranscriptionMode } from './types'
140
+ export type {
141
+ ConversationsApi,
142
+ SSEProvider,
143
+ ConversationEventSource,
144
+ ConversationSummary,
145
+ ConversationDocument,
146
+ ConversationPage,
147
+ ConversationDocumentPage,
148
+ CompanyDocument,
149
+ CompanyDocumentPage,
150
+ ConversationTemplate,
151
+ ListConversationsParams,
152
+ ListDocumentsParams,
153
+ } from './providers/types'
154
+ export type { UseConversationActionsResult, UseInboxActionsResult } from './hooks/useConversationActions'
48
155
 
49
156
  export type { MessageBubbleProps } from './MessageBubble'
50
157
  export type { ConversationWallpaperProps } from './Wallpaper'
51
158
  export type { ConversationLocales, ConversationLocalesProviderProps } from './ConversationLocalesProvider'
52
159
  export type { AudioPlayerProps } from './AudioPlayer'
53
- export type { EmojiPickerProps } from './EmojiPicker'
54
- export type { MessageComposerProps } from './MessageComposer'
55
- export type { WhatsAppMessageEditorProps } from './WhatsAppMessageEditor'
160
+ export type { AudioTranscriptionProps } from './AudioTranscription'
161
+ export type { EmojiPickerProps, EmojiPickerLabels } from './EmojiPicker'
162
+ export type { EmojiEntry, EmojiCategory } from './emojiCatalog'
163
+ export type { InteractiveMessageProps, InteractiveMessageLabels } from './InteractiveMessage'
164
+ export type { MessageComposerProps, MessageComposerClassNames, MessageComposerLabels } from './MessageComposer'
165
+ export type { WhatsAppMessageEditorProps, WhatsAppMessageEditorLabels } from './WhatsAppMessageEditor'
56
166
  export type { SimpleEmojiPickerProps } from './SimpleEmojiPicker'
57
- export type { DateDividerProps } from './DateDivider'
58
- export type { AvatarProps } from './Avatar'
59
- export type { ConversationListItemProps } from './ConversationListItem'
167
+ export type { DateDividerProps, DateDividerClassNames } from './DateDivider'
168
+ export type { AvatarProps, AvatarLabels } from './Avatar'
169
+ export type { ConversationListItemProps, ConversationListItemLabels } from './ConversationListItem'
60
170
  export type { StatusTicksProps } from './StatusTicks'
61
- export type { LightboxProps } from './Lightbox'
171
+ export type { LightboxProps, LightboxLabels } from './Lightbox'
62
172
  export type { MediaRendererProps, ResolveMediaUrl } from './MediaRenderer'
63
173
  export type { FileIconProps } from './FileIcon'
64
174
  export type { MessageTextProps } from './MessageText'
@@ -77,11 +187,25 @@ export type {
77
187
  WhatsAppCreateTemplateFormLabels,
78
188
  } from './settings/WhatsAppCreateTemplateForm'
79
189
  export type { WelcomeFarewellFormProps, WelcomeFarewellFormLabels } from './settings/WelcomeFarewellForm'
190
+ export type {
191
+ TranscriptionSettingsFormProps,
192
+ TranscriptionSettingsFormLabels,
193
+ } from './settings/TranscriptionSettingsForm'
194
+ export type {
195
+ WhatsAppTemplatesSettingsProps,
196
+ WhatsAppTemplatesSettingsLabels,
197
+ TemplateSettingsTab,
198
+ } from './settings/WhatsAppTemplatesSettings'
80
199
  export type { TopicsFormProps, TopicItem, TopicsFormLabels } from './settings/TopicsForm'
81
200
 
82
201
  export type { UseConversationMessagesResult } from './hooks/useConversationMessages'
83
202
  export type { UseConversationListParams, UseConversationListResult } from './hooks/useConversationList'
84
203
  export type { UseConversationContextResult } from './hooks/useConversationContext'
204
+ export type { UseScrollToLatestMessageParams, UseScrollToLatestMessageResult } from './hooks/useScrollToLatestMessage'
85
205
  export type { UseConversationDocumentsParams, UseConversationDocumentsResult } from './hooks/useConversationDocuments'
86
206
  export type { ConversationRealtimeHandler } from './hooks/useConversationRealtime'
87
207
  export type { AsyncResourceState } from './hooks/useAsyncResource'
208
+ export { createMediaUrlResolver } from './lib/createMediaUrlResolver'
209
+ export type { ConversationHeaderUtility } from './ConversationHeader'
210
+ export { applyQuickReplyVariables, resolveQuickReply } from './MessageComposer'
211
+ 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
+ }
package/src/lib/phone.ts CHANGED
@@ -20,6 +20,40 @@ export function formatPhone(number: string): string {
20
20
  return number
21
21
  }
22
22
 
23
+ // Mapa explícito em vez de derivar o emoji do código: prefixos são ambíguos (1 = EUA e Canadá,
24
+ // 7 = Rússia e Cazaquistão) e mostrar a bandeira errada é pior do que não mostrar nenhuma.
25
+ const COUNTRY_FLAG_BY_DIAL_CODE: Readonly<Record<string, string>> = {
26
+ '55': '🇧🇷',
27
+ '351': '🇵🇹',
28
+ '34': '🇪🇸',
29
+ '54': '🇦🇷',
30
+ '56': '🇨🇱',
31
+ '57': '🇨🇴',
32
+ '52': '🇲🇽',
33
+ '598': '🇺🇾',
34
+ '595': '🇵🇾',
35
+ '44': '🇬🇧',
36
+ '49': '🇩🇪',
37
+ '39': '🇮🇹',
38
+ '33': '🇫🇷',
39
+ }
40
+
41
+ /**
42
+ * Devolve string vazia quando não reconhece o país — quem renderiza decide se some com o espaço.
43
+ */
44
+ export function phoneCountryFlag(number: string): string {
45
+ const digits = number.replace(/\D/g, '')
46
+
47
+ // Do prefixo mais longo para o mais curto: '55' casaria antes de '551' e daria bandeira errada
48
+ // em países de código de três dígitos.
49
+ for (const length of [3, 2, 1]) {
50
+ const flag = COUNTRY_FLAG_BY_DIAL_CODE[digits.slice(0, length)]
51
+ if (flag) return flag
52
+ }
53
+
54
+ return ''
55
+ }
56
+
23
57
  export function phoneInitials(number: string): string {
24
58
  const digits = number.replace(/\D/g, '')
25
59
  return digits.slice(-2)