@adatechnology/conversations-ui 0.1.0-rc.4 → 0.1.0-rc.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ConversationSimulatorPanel--5fIzXWY.d.ts +804 -0
- package/dist/chunk-BJNRLLDO.js +2708 -0
- package/dist/chunk-DKPXKQGC.js +110 -0
- package/dist/{chunk-OGRRHQQW.js → chunk-WCBDXZ3X.js} +68 -4
- package/dist/flows/index.d.ts +422 -5
- package/dist/flows/index.js +2389 -678
- package/dist/index.d.ts +1171 -42
- package/dist/index.js +3755 -843
- package/dist/preview/index.d.ts +157 -42
- package/dist/preview/index.js +772 -191
- package/dist/styles.css +893 -0
- package/package.json +9 -8
- package/src/AudioPlayer.tsx +8 -0
- package/src/AudioRecorderButton.test.tsx +30 -0
- package/src/AudioRecorderButton.tsx +248 -0
- package/src/AudioTranscription.test.tsx +115 -0
- package/src/AudioTranscription.tsx +252 -0
- package/src/Avatar.tsx +14 -3
- package/src/ConversationContextPanel.tsx +218 -44
- package/src/ConversationDocumentsPanel.tsx +347 -24
- package/src/ConversationHeader.test.tsx +66 -0
- package/src/ConversationHeader.tsx +163 -45
- package/src/ConversationListItem.tsx +19 -2
- package/src/ConversationLocalesProvider.tsx +28 -0
- package/src/ConversationRow.tsx +31 -7
- package/src/DarkModeToggle.test.tsx +76 -0
- package/src/DarkModeToggle.tsx +92 -0
- package/src/DocumentsLibrary.tsx +382 -0
- package/src/EmojiPicker.tsx +70 -55
- package/src/FileIcon.test.ts +83 -0
- package/src/FileIcon.tsx +88 -11
- package/src/InteractiveMessage.test.tsx +41 -0
- package/src/InteractiveMessage.tsx +146 -0
- package/src/Lightbox.tsx +18 -3
- package/src/MediaRenderer.tsx +92 -16
- package/src/MessageBubble.test.tsx +41 -0
- package/src/MessageBubble.tsx +75 -6
- package/src/MessageComposer.test.tsx +35 -0
- package/src/MessageComposer.tsx +155 -19
- package/src/RichMessageComposer.test.tsx +113 -0
- package/src/RichMessageComposer.tsx +551 -0
- package/src/SimpleEmojiPicker.tsx +5 -3
- package/src/StatusTicks.tsx +1 -1
- package/src/Toast.tsx +4 -0
- package/src/Tooltip.test.ts +42 -0
- package/src/Tooltip.tsx +167 -0
- package/src/Wallpaper.test.tsx +21 -0
- package/src/Wallpaper.tsx +67 -7
- package/src/WhatsAppMessageEditor.tsx +34 -7
- package/src/WindowExpiredNotice.tsx +12 -4
- package/src/audioRecorderFormat.test.ts +67 -0
- package/src/buildOutput.test.ts +79 -0
- package/src/composer.constant.ts +33 -0
- package/src/conversationTranscript.test.ts +57 -0
- package/src/conversationTranscript.ts +29 -4
- package/src/conversationWindow.ts +7 -5
- package/src/documentTypeLabel.test.ts +57 -0
- package/src/documents/DocumentsWorkspace.tsx +550 -0
- package/src/documents/index.ts +8 -0
- package/src/documents/labels.ts +92 -0
- package/src/emojiCatalog.test.ts +35 -0
- package/src/emojiCatalog.ts +189 -0
- package/src/flows/FlowConnectionEdge.tsx +104 -0
- package/src/flows/FlowGroupHeader.tsx +12 -2
- package/src/flows/FlowLegend.tsx +125 -0
- package/src/flows/FlowMapCanvas.tsx +15 -12
- package/src/flows/FlowMapNode.tsx +4 -1
- package/src/flows/FlowNodeCard.tsx +219 -34
- package/src/flows/FlowNodePanel.tsx +153 -39
- package/src/flows/FlowPalette.tsx +106 -69
- package/src/flows/FlowPortalNode.tsx +1 -1
- package/src/flows/FlowWhatsAppPreview.tsx +14 -3
- package/src/flows/FlowsWorkspace.tsx +1219 -0
- package/src/flows/flowCanvasModel.test.ts +456 -0
- package/src/flows/flowCanvasModel.ts +378 -0
- package/src/flows/flowEditorOps.test.ts +276 -0
- package/src/flows/flowEditorOps.ts +202 -0
- package/src/flows/flowGraph.ts +78 -53
- package/src/flows/index.ts +51 -2
- package/src/flows/labels.ts +180 -0
- package/src/flows/workspaceContract.test.ts +126 -0
- package/src/hooks/useContainerWidth.ts +35 -0
- package/src/hooks/useConversationActions.ts +56 -0
- package/src/hooks/useConversationDocuments.ts +11 -7
- package/src/hooks/useConversationList.ts +15 -9
- package/src/hooks/useConversationMessages.ts +2 -2
- package/src/hooks/useConversationRealtime.ts +10 -8
- package/src/hooks/useScrollToLatestMessage.ts +127 -0
- package/src/hooks/useUrlFilterState.ts +107 -0
- package/src/icon.constant.ts +12 -0
- package/src/index.ts +129 -13
- package/src/lib/cn.test.ts +29 -0
- package/src/lib/composer-formatting.test.ts +78 -0
- package/src/lib/composer-formatting.ts +145 -0
- package/src/lib/createMediaUrlResolver.ts +33 -0
- package/src/lib/paginated.test.ts +33 -0
- package/src/lib/paginated.ts +26 -0
- package/src/lib/whatsapp-formatting.test.tsx +37 -0
- package/src/lib/whatsapp-formatting.tsx +28 -3
- package/src/listing/index.tsx +202 -0
- package/src/pagination.constant.ts +10 -0
- package/src/preview/ConversationPreview.tsx +225 -17
- package/src/preview/ConversationSimulatorClient.ts +143 -0
- package/src/preview/ConversationSimulatorPanel.test.tsx +55 -0
- package/src/preview/ConversationSimulatorPanel.tsx +131 -0
- package/src/preview/MediaTypesPreview.tsx +87 -0
- package/src/preview/conversationPreviewFailures.test.ts +64 -0
- package/src/preview/createMockConversationsApi.ts +175 -15
- package/src/preview/createPreviewBridgeClient.test.ts +92 -0
- package/src/preview/createPreviewBridgeClient.ts +124 -0
- package/src/preview/createPreviewMediaUploader.ts +82 -0
- package/src/preview/createPreviewWebhookClient.test.ts +96 -0
- package/src/preview/createPreviewWebhookClient.ts +127 -4
- package/src/preview/index.ts +51 -3
- package/src/preview/mediaTypeOf.test.ts +15 -0
- package/src/preview/mockDocumentsSearch.test.ts +57 -0
- package/src/preview/preview.test.ts +5 -3
- package/src/preview/previewFileSamples.test.ts +151 -0
- package/src/preview/previewFileSamples.ts +74 -0
- package/src/preview/previewFixtures.ts +288 -1
- package/src/preview/previewMediaSource.test.ts +62 -0
- package/src/preview/previewMediaSource.ts +91 -0
- package/src/preview/previewMediaUploader.test.ts +61 -0
- package/src/providers/ConversationsProvider.tsx +8 -6
- package/src/providers/types.ts +185 -10
- package/src/quickReply.test.ts +58 -0
- package/src/settings/MessagesWorkspace.tsx +571 -0
- package/src/settings/TopicsForm.tsx +2 -0
- package/src/settings/TranscriptionSettingsForm.test.tsx +81 -0
- package/src/settings/TranscriptionSettingsForm.tsx +190 -0
- package/src/settings/WelcomeFarewellForm.tsx +1 -0
- package/src/settings/WhatsAppCreateTemplateForm.tsx +4 -1
- package/src/settings/WhatsAppTemplateSettingsForm.tsx +5 -2
- package/src/settings/WhatsAppTemplatesSettings.test.tsx +61 -0
- package/src/settings/WhatsAppTemplatesSettings.tsx +22 -2
- package/src/styles.css +858 -0
- package/src/types.ts +64 -1
- package/src/useWaitingNotifications.ts +74 -29
- package/src/workspace/BulkTemplateModal.tsx +132 -0
- package/src/workspace/ConversationPane.tsx +432 -0
- package/src/workspace/ConversationsInboxList.tsx +194 -0
- package/src/workspace/ConversationsWorkspace.tsx +423 -0
- package/src/workspace/index.ts +17 -0
- package/src/workspace/labels.test.ts +17 -0
- package/src/workspace/labels.ts +85 -0
- package/src/workspace/useConversationsInbox.ts +332 -0
- package/dist/chunk-4R6Y43DQ.js +0 -726
- package/dist/chunk-NV2RZ5KT.js +0 -56
- package/dist/types-C0PtaO7S.d.ts +0 -207
|
@@ -0,0 +1,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
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Estado de listagem espelhado na URL — ordenação, filtros e paginação (regra `web.md` §7).
|
|
3
|
+
*
|
|
4
|
+
* Escrito sobre `history.replaceState` e não sobre um router: o pacote roda em três produtos com
|
|
5
|
+
* routers diferentes, e exigir um deles arrastaria dependência de framework para dentro do módulo.
|
|
6
|
+
* `replaceState` também é o comportamento certo aqui — filtrar não é navegar, e cada tecla digitada
|
|
7
|
+
* na busca não deve virar uma entrada no botão "voltar".
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { useCallback, useEffect, useState } from 'react'
|
|
11
|
+
|
|
12
|
+
const LIST_SEPARATOR = ','
|
|
13
|
+
|
|
14
|
+
function readParams(): URLSearchParams {
|
|
15
|
+
if (typeof window === 'undefined') return new URLSearchParams()
|
|
16
|
+
return new URLSearchParams(window.location.search)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function writeParam(key: string, value: string | undefined): void {
|
|
20
|
+
if (typeof window === 'undefined') return
|
|
21
|
+
const params = readParams()
|
|
22
|
+
if (value === undefined || value === '') params.delete(key)
|
|
23
|
+
else params.set(key, value)
|
|
24
|
+
const query = params.toString()
|
|
25
|
+
window.history.replaceState(null, '', query ? `${window.location.pathname}?${query}` : window.location.pathname)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** `enabled: false` mantém o mesmo contrato de estado sem tocar na URL — para uso em preview e teste. */
|
|
29
|
+
export interface UrlStateOptions {
|
|
30
|
+
readonly enabled?: boolean
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function useUrlStringState(
|
|
34
|
+
key: string,
|
|
35
|
+
initial: string,
|
|
36
|
+
{ enabled = true }: UrlStateOptions = {},
|
|
37
|
+
): [string, (next: string) => void] {
|
|
38
|
+
const [value, setValue] = useState(() => (enabled ? (readParams().get(key) ?? initial) : initial))
|
|
39
|
+
|
|
40
|
+
const update = useCallback(
|
|
41
|
+
(next: string) => {
|
|
42
|
+
setValue(next)
|
|
43
|
+
if (enabled) writeParam(key, next === initial ? undefined : next)
|
|
44
|
+
},
|
|
45
|
+
[enabled, key, initial],
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
return [value, update]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function useUrlNumberState(
|
|
52
|
+
key: string,
|
|
53
|
+
initial: number,
|
|
54
|
+
{ enabled = true }: UrlStateOptions = {},
|
|
55
|
+
): [number, (next: number) => void] {
|
|
56
|
+
const [value, setValue] = useState(() => {
|
|
57
|
+
if (!enabled) return initial
|
|
58
|
+
const raw = Number(readParams().get(key))
|
|
59
|
+
return Number.isFinite(raw) && raw > 0 ? raw : initial
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
const update = useCallback(
|
|
63
|
+
(next: number) => {
|
|
64
|
+
setValue(next)
|
|
65
|
+
if (enabled) writeParam(key, next === initial ? undefined : String(next))
|
|
66
|
+
},
|
|
67
|
+
[enabled, key, initial],
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
return [value, update]
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function useUrlArrayState(
|
|
74
|
+
key: string,
|
|
75
|
+
{ enabled = true }: UrlStateOptions = {},
|
|
76
|
+
): [readonly string[], (next: readonly string[]) => void] {
|
|
77
|
+
const [value, setValue] = useState<readonly string[]>(() => {
|
|
78
|
+
if (!enabled) return []
|
|
79
|
+
const raw = readParams().get(key)
|
|
80
|
+
return raw ? raw.split(LIST_SEPARATOR).filter(Boolean) : []
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
const update = useCallback(
|
|
84
|
+
(next: readonly string[]) => {
|
|
85
|
+
setValue(next)
|
|
86
|
+
if (enabled) writeParam(key, next.length > 0 ? next.join(LIST_SEPARATOR) : undefined)
|
|
87
|
+
},
|
|
88
|
+
[enabled, key],
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
return [value, update]
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Espera o usuário parar de digitar antes de deixar o valor chegar à query. Sem isto, cada tecla
|
|
96
|
+
* na busca vira uma chamada de rede e uma reescrita de URL.
|
|
97
|
+
*/
|
|
98
|
+
export function useDebouncedValue<TValue>(value: TValue, delayMs = 300): TValue {
|
|
99
|
+
const [debounced, setDebounced] = useState(value)
|
|
100
|
+
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
const timer = setTimeout(() => setDebounced(value), delayMs)
|
|
103
|
+
return () => clearTimeout(timer)
|
|
104
|
+
}, [value, delayMs])
|
|
105
|
+
|
|
106
|
+
return debounced
|
|
107
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tamanhos de ícone da UI de conversas, em px, na escala de 4 da grade.
|
|
3
|
+
*
|
|
4
|
+
* Ícone vem da biblioteca (lucide), nunca emoji. Emoji desenha diferente em cada sistema
|
|
5
|
+
* operacional, não herda `currentColor` — então não acompanha o estado do controle (ativo,
|
|
6
|
+
* desabilitado, variante destrutiva) — e não escala com o token de tipografia. Numa mesma faixa,
|
|
7
|
+
* metade emoji e metade SVG lê como duas famílias de botão.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const ICON_SIZE_PILL = 12
|
|
11
|
+
export const ICON_SIZE_INLINE = 14
|
|
12
|
+
export const ICON_SIZE_ACTION = 16
|
package/src/index.ts
CHANGED
|
@@ -1,18 +1,37 @@
|
|
|
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 {
|
|
6
|
-
export {
|
|
7
|
-
export {
|
|
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'
|
|
31
|
+
export { TooltipLayer, TOOLTIP_ATTRIBUTE } from './Tooltip'
|
|
13
32
|
|
|
14
33
|
export { StatusTicks } from './StatusTicks'
|
|
15
|
-
export { Lightbox } from './Lightbox'
|
|
34
|
+
export { Lightbox, DEFAULT_LIGHTBOX_LABELS } from './Lightbox'
|
|
16
35
|
export { MediaRenderer } from './MediaRenderer'
|
|
17
36
|
export { FileIcon } from './FileIcon'
|
|
18
37
|
export { MessageText } from './MessageText'
|
|
@@ -60,11 +79,34 @@ export { ConversationContextPanel, DEFAULT_CONVERSATION_CONTEXT_LABELS } from '.
|
|
|
60
79
|
export type {
|
|
61
80
|
ConversationContextPanelProps,
|
|
62
81
|
ConversationContextEntry,
|
|
82
|
+
ConversationContextStatus,
|
|
63
83
|
ConversationContextPanelLabels,
|
|
64
84
|
ConversationContextPanelClassNames,
|
|
65
85
|
} from './ConversationContextPanel'
|
|
66
86
|
export { WindowExpiredNotice, isWindowBlocking, DEFAULT_WINDOW_EXPIRED_LABELS } from './WindowExpiredNotice'
|
|
67
87
|
export type { WindowExpiredNoticeProps, WindowExpiredNoticeLabels } from './WindowExpiredNotice'
|
|
88
|
+
export { DocumentsLibrary, DEFAULT_DOCUMENTS_LIBRARY_LABELS } from './DocumentsLibrary'
|
|
89
|
+
export type { DocumentsLibraryProps, DocumentsLibraryLabels, DocumentsLibraryClassNames } from './DocumentsLibrary'
|
|
90
|
+
export { DocumentsWorkspace, DEFAULT_DOCUMENTS_WORKSPACE_LABELS } from './documents'
|
|
91
|
+
export type {
|
|
92
|
+
DocumentsWorkspaceProps,
|
|
93
|
+
DocumentsWorkspaceLabels,
|
|
94
|
+
DocumentsWorkspaceClassNames,
|
|
95
|
+
DocumentsFiltersContext,
|
|
96
|
+
} from './documents'
|
|
97
|
+
export { SortableHead, MultiSelectFilter, BulkActionBar, ListingPagination } from './listing'
|
|
98
|
+
export type {
|
|
99
|
+
SortableHeadProps,
|
|
100
|
+
MultiSelectFilterProps,
|
|
101
|
+
BulkActionBarProps,
|
|
102
|
+
ListingPaginationProps,
|
|
103
|
+
FilterOption,
|
|
104
|
+
SortDirection,
|
|
105
|
+
} from './listing'
|
|
106
|
+
export { useUrlStringState, useUrlNumberState, useUrlArrayState, useDebouncedValue } from './hooks/useUrlFilterState'
|
|
107
|
+
export type { UrlStateOptions } from './hooks/useUrlFilterState'
|
|
108
|
+
export { DOCUMENT_SOURCE_FILTER } from './ConversationDocumentsPanel'
|
|
109
|
+
export type { DocumentSourceFilter } from './ConversationDocumentsPanel'
|
|
68
110
|
export type { ConversationDocumentsPanelClassNames } from './ConversationDocumentsPanel'
|
|
69
111
|
export { ConversationDocumentsPanel, DEFAULT_CONVERSATION_DOCUMENTS_LABELS } from './ConversationDocumentsPanel'
|
|
70
112
|
export type { ConversationDocumentsPanelProps, ConversationDocumentsPanelLabels } from './ConversationDocumentsPanel'
|
|
@@ -72,8 +114,15 @@ export { buildTranscriptText, buildTranscriptFilename, downloadTextFile } from '
|
|
|
72
114
|
export type { BuildTranscriptTextParams } from './conversationTranscript'
|
|
73
115
|
|
|
74
116
|
export { useDarkMode, useIsDarkTheme } from './useDarkMode'
|
|
117
|
+
export { DarkModeToggle, DEFAULT_DARK_MODE_TOGGLE_LABELS } from './DarkModeToggle'
|
|
118
|
+
export type { DarkModeToggleProps, DarkModeToggleLabels } from './DarkModeToggle'
|
|
75
119
|
export { useIsNarrow, NARROW_MAX_WIDTH_PX } from './useIsNarrow'
|
|
76
120
|
export { useWaitingNotifications } from './useWaitingNotifications'
|
|
121
|
+
export type {
|
|
122
|
+
UseWaitingNotificationsLabels,
|
|
123
|
+
UseWaitingNotificationsParams,
|
|
124
|
+
UseWaitingNotificationsResult,
|
|
125
|
+
} from './useWaitingNotifications'
|
|
77
126
|
|
|
78
127
|
export { ConversationsProvider, useConversations } from './providers/ConversationsProvider'
|
|
79
128
|
|
|
@@ -82,6 +131,7 @@ export { ConversationsProvider, useConversations } from './providers/Conversatio
|
|
|
82
131
|
export { WhatsAppTemplateSettingsForm } from './settings/WhatsAppTemplateSettingsForm'
|
|
83
132
|
export { WhatsAppCreateTemplateForm } from './settings/WhatsAppCreateTemplateForm'
|
|
84
133
|
export { WelcomeFarewellForm } from './settings/WelcomeFarewellForm'
|
|
134
|
+
export { TranscriptionSettingsForm } from './settings/TranscriptionSettingsForm'
|
|
85
135
|
export {
|
|
86
136
|
WhatsAppTemplatesSettings,
|
|
87
137
|
TEMPLATE_SETTINGS_TAB,
|
|
@@ -89,40 +139,70 @@ export {
|
|
|
89
139
|
} from './settings/WhatsAppTemplatesSettings'
|
|
90
140
|
export { TopicsForm } from './settings/TopicsForm'
|
|
91
141
|
|
|
142
|
+
// Tela composta de Mensagens: junta os formulários acima com abas, estado e salvamento, para o
|
|
143
|
+
// host não remontar essa mesma colagem em cada produto (foi assim que eles divergiram).
|
|
144
|
+
export { MessagesWorkspace } from './settings/MessagesWorkspace'
|
|
145
|
+
export type {
|
|
146
|
+
MessagesWorkspaceProps,
|
|
147
|
+
MessagesWorkspaceApi,
|
|
148
|
+
MessagesWorkspaceLabels,
|
|
149
|
+
MessagesWorkspaceTemplateRole,
|
|
150
|
+
BotMessages,
|
|
151
|
+
TemplateSettings,
|
|
152
|
+
TranscriptionSettings,
|
|
153
|
+
} from './settings/MessagesWorkspace'
|
|
154
|
+
|
|
92
155
|
// Camada headless (T6.9) — hooks de dados/ações independentes de qualquer tela, para o
|
|
93
156
|
// produto montar sua própria UI sobre eles. Requerem <ConversationsProvider> como ancestral.
|
|
94
157
|
export { useConversationMessages } from './hooks/useConversationMessages'
|
|
158
|
+
export { useScrollToLatestMessage } from './hooks/useScrollToLatestMessage'
|
|
95
159
|
export { useConversationList } from './hooks/useConversationList'
|
|
96
160
|
export { useConversationContext } from './hooks/useConversationContext'
|
|
97
161
|
export { useConversationDocuments } from './hooks/useConversationDocuments'
|
|
98
162
|
export { useConversationRealtime, useGlobalRealtime } from './hooks/useConversationRealtime'
|
|
163
|
+
export { useConversationActions, useInboxActions } from './hooks/useConversationActions'
|
|
99
164
|
|
|
100
165
|
export { parseWhatsAppFormatting, waToHTML, htmlToWA, waToHTMLInline } from './lib/whatsapp-formatting'
|
|
101
166
|
export { formatPhone, phoneInitials } from './lib/phone'
|
|
102
|
-
|
|
167
|
+
// `formatDateTime` e `isSameDay` já eram usados pelas bolhas e pelo divisor de data; exportá-los
|
|
168
|
+
// evita que cada host mantenha a própria cópia e acabe com timeline e transcript divergindo.
|
|
169
|
+
export { formatTimestamp, formatFileSize, formatDateTime, isSameDay } from './lib/format'
|
|
103
170
|
|
|
104
171
|
export type { MessagePayload, ConversationsUIConfig, ConversationsTheme, ConversationsFeatures } from './types'
|
|
172
|
+
export type { InteractivePayload, InteractiveSection, InteractiveOption, InteractiveSelection } from './types'
|
|
173
|
+
export type { MessageTranscription, TranscriptionStatus, TranscriptionMode } from './types'
|
|
105
174
|
export type {
|
|
106
175
|
ConversationsApi,
|
|
107
176
|
SSEProvider,
|
|
108
177
|
ConversationEventSource,
|
|
109
178
|
ConversationSummary,
|
|
110
179
|
ConversationDocument,
|
|
180
|
+
ConversationPage,
|
|
181
|
+
ConversationDocumentPage,
|
|
182
|
+
CompanyDocument,
|
|
183
|
+
CompanyDocumentPage,
|
|
184
|
+
ConversationTemplate,
|
|
185
|
+
ListConversationsParams,
|
|
186
|
+
ListDocumentsParams,
|
|
111
187
|
} from './providers/types'
|
|
188
|
+
export type { UseConversationActionsResult, UseInboxActionsResult } from './hooks/useConversationActions'
|
|
112
189
|
|
|
113
190
|
export type { MessageBubbleProps } from './MessageBubble'
|
|
114
191
|
export type { ConversationWallpaperProps } from './Wallpaper'
|
|
115
192
|
export type { ConversationLocales, ConversationLocalesProviderProps } from './ConversationLocalesProvider'
|
|
116
193
|
export type { AudioPlayerProps } from './AudioPlayer'
|
|
117
|
-
export type {
|
|
118
|
-
export type {
|
|
119
|
-
export type {
|
|
194
|
+
export type { AudioTranscriptionProps } from './AudioTranscription'
|
|
195
|
+
export type { EmojiPickerProps, EmojiPickerLabels } from './EmojiPicker'
|
|
196
|
+
export type { EmojiEntry, EmojiCategory } from './emojiCatalog'
|
|
197
|
+
export type { InteractiveMessageProps, InteractiveMessageLabels } from './InteractiveMessage'
|
|
198
|
+
export type { MessageComposerProps, MessageComposerClassNames, MessageComposerLabels } from './MessageComposer'
|
|
199
|
+
export type { WhatsAppMessageEditorProps, WhatsAppMessageEditorLabels } from './WhatsAppMessageEditor'
|
|
120
200
|
export type { SimpleEmojiPickerProps } from './SimpleEmojiPicker'
|
|
121
201
|
export type { DateDividerProps, DateDividerClassNames } from './DateDivider'
|
|
122
|
-
export type { AvatarProps } from './Avatar'
|
|
123
|
-
export type { ConversationListItemProps } from './ConversationListItem'
|
|
202
|
+
export type { AvatarProps, AvatarLabels } from './Avatar'
|
|
203
|
+
export type { ConversationListItemProps, ConversationListItemLabels } from './ConversationListItem'
|
|
124
204
|
export type { StatusTicksProps } from './StatusTicks'
|
|
125
|
-
export type { LightboxProps } from './Lightbox'
|
|
205
|
+
export type { LightboxProps, LightboxLabels } from './Lightbox'
|
|
126
206
|
export type { MediaRendererProps, ResolveMediaUrl } from './MediaRenderer'
|
|
127
207
|
export type { FileIconProps } from './FileIcon'
|
|
128
208
|
export type { MessageTextProps } from './MessageText'
|
|
@@ -141,6 +221,10 @@ export type {
|
|
|
141
221
|
WhatsAppCreateTemplateFormLabels,
|
|
142
222
|
} from './settings/WhatsAppCreateTemplateForm'
|
|
143
223
|
export type { WelcomeFarewellFormProps, WelcomeFarewellFormLabels } from './settings/WelcomeFarewellForm'
|
|
224
|
+
export type {
|
|
225
|
+
TranscriptionSettingsFormProps,
|
|
226
|
+
TranscriptionSettingsFormLabels,
|
|
227
|
+
} from './settings/TranscriptionSettingsForm'
|
|
144
228
|
export type {
|
|
145
229
|
WhatsAppTemplatesSettingsProps,
|
|
146
230
|
WhatsAppTemplatesSettingsLabels,
|
|
@@ -151,6 +235,38 @@ export type { TopicsFormProps, TopicItem, TopicsFormLabels } from './settings/To
|
|
|
151
235
|
export type { UseConversationMessagesResult } from './hooks/useConversationMessages'
|
|
152
236
|
export type { UseConversationListParams, UseConversationListResult } from './hooks/useConversationList'
|
|
153
237
|
export type { UseConversationContextResult } from './hooks/useConversationContext'
|
|
238
|
+
export type { UseScrollToLatestMessageParams, UseScrollToLatestMessageResult } from './hooks/useScrollToLatestMessage'
|
|
154
239
|
export type { UseConversationDocumentsParams, UseConversationDocumentsResult } from './hooks/useConversationDocuments'
|
|
155
240
|
export type { ConversationRealtimeHandler } from './hooks/useConversationRealtime'
|
|
156
241
|
export type { AsyncResourceState } from './hooks/useAsyncResource'
|
|
242
|
+
export { createMediaUrlResolver } from './lib/createMediaUrlResolver'
|
|
243
|
+
export type { ConversationHeaderUtility } from './ConversationHeader'
|
|
244
|
+
export { applyQuickReplyVariables, resolveQuickReply } from './MessageComposer'
|
|
245
|
+
export type { QuickReply } from './MessageComposer'
|
|
246
|
+
|
|
247
|
+
// Tela de atendimento completa. Fica no export principal — e não num subpath — porque é a
|
|
248
|
+
// composição padrão do pacote: quem instala conversas quer esta tela, e as peças continuam
|
|
249
|
+
// exportadas ao lado para quem precisar montar outra.
|
|
250
|
+
export { ConversationsWorkspace, ConversationPane, ConversationsInboxList } from './workspace'
|
|
251
|
+
export { useConversationsInbox, CONVERSATIONS_PER_PAGE, DEFAULT_CONVERSATIONS_WORKSPACE_LABELS } from './workspace'
|
|
252
|
+
export type {
|
|
253
|
+
ConversationsWorkspaceProps,
|
|
254
|
+
ConversationsWorkspaceSimulator,
|
|
255
|
+
SimulatorTransportFactory,
|
|
256
|
+
SimulatorTransportParams,
|
|
257
|
+
ConversationsWorkspaceLabels,
|
|
258
|
+
ConversationPaneProps,
|
|
259
|
+
ConversationsInboxListProps,
|
|
260
|
+
UseConversationsInboxParams,
|
|
261
|
+
UseConversationsInboxResult,
|
|
262
|
+
} from './workspace'
|
|
263
|
+
|
|
264
|
+
// A porta do simulador entra no export principal como TIPO: `simulator.transports` é tipado por ela,
|
|
265
|
+
// e obrigar o host a abrir o subpath `/preview` só para declarar a fábrica seria pedir que ele
|
|
266
|
+
// importasse fixtures para escrever uma assinatura. `export type` é apagado no build — o `preview/`
|
|
267
|
+
// continua fora do bundle de quem só declara o transporte.
|
|
268
|
+
export type {
|
|
269
|
+
ConversationSimulatorClient,
|
|
270
|
+
SendSimulatorMediaParams,
|
|
271
|
+
SimulatorMediaKind,
|
|
272
|
+
} from './preview/ConversationSimulatorClient'
|
|
@@ -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
|
+
})
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
FORMATTING_ACTION,
|
|
5
|
+
FORMATTING_SEPARATOR,
|
|
6
|
+
activeFormattingIn,
|
|
7
|
+
canExecuteFormattingCommand,
|
|
8
|
+
isFormattingActive,
|
|
9
|
+
toggleFormattingCommand,
|
|
10
|
+
} from './composer-formatting'
|
|
11
|
+
import { ZERO_WIDTH_SPACE, htmlToWA, waToHTML } from './whatsapp-formatting'
|
|
12
|
+
|
|
13
|
+
describe('isFormattingActive', () => {
|
|
14
|
+
it('reconhece cada formatação dentro do conjunto ativo', () => {
|
|
15
|
+
const active = [FORMATTING_ACTION.BOLD, FORMATTING_ACTION.MONOSPACE].join(FORMATTING_SEPARATOR)
|
|
16
|
+
|
|
17
|
+
expect(isFormattingActive({ active, action: FORMATTING_ACTION.BOLD })).toBe(true)
|
|
18
|
+
expect(isFormattingActive({ active, action: FORMATTING_ACTION.MONOSPACE })).toBe(true)
|
|
19
|
+
expect(isFormattingActive({ active, action: FORMATTING_ACTION.ITALIC })).toBe(false)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('não acende nada quando o conjunto está vazio', () => {
|
|
23
|
+
expect(isFormattingActive({ active: '', action: FORMATTING_ACTION.BOLD })).toBe(false)
|
|
24
|
+
})
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
describe('activeFormattingIn', () => {
|
|
28
|
+
it('não acende nada quando não há seleção no documento', () => {
|
|
29
|
+
const editor = { contains: () => false } as unknown as HTMLElement
|
|
30
|
+
|
|
31
|
+
expect(activeFormattingIn(editor)).toBe('')
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
describe('toggleFormattingCommand', () => {
|
|
36
|
+
it('avisa que não executou onde o comando não existe, para o campo cair no wrap manual', () => {
|
|
37
|
+
if (canExecuteFormattingCommand()) return
|
|
38
|
+
expect(toggleFormattingCommand(FORMATTING_ACTION.BOLD)).toBe(false)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('não tem comando nativo para monoespaçado', () => {
|
|
42
|
+
expect(toggleFormattingCommand(FORMATTING_ACTION.MONOSPACE)).toBe(false)
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
describe('waToHTML dentro do campo editável', () => {
|
|
47
|
+
it('escreve o tachado como <s>, que é o que o navegador consegue desfazer', () => {
|
|
48
|
+
expect(waToHTML('~riscado~')).toBe('<s>riscado</s>')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('mantém o ida e volta do tachado', () => {
|
|
52
|
+
expect(htmlToWA(waToHTML('~riscado~'))).toBe('~riscado~')
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
describe('htmlToWA e a âncora do cursor', () => {
|
|
57
|
+
it('descarta o código que só tem a âncora, para não sair um par de crases vazio', () => {
|
|
58
|
+
expect(htmlToWA(`oi <code>${ZERO_WIDTH_SPACE}</code>`)).toBe('oi')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('tira a âncora de dentro do código que o operador chegou a preencher', () => {
|
|
62
|
+
expect(htmlToWA(`<code>${ZERO_WIDTH_SPACE}npm run dev</code>`)).toBe('`npm run dev`')
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('não deixa a âncora vazar para o texto enviado', () => {
|
|
66
|
+
expect(htmlToWA(`oi${ZERO_WIDTH_SPACE} tudo bem`)).toBe('oi tudo bem')
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
describe('htmlToWA com a marcação do navegador', () => {
|
|
71
|
+
it('converte o <strike> do execCommand para o tachado do WhatsApp', () => {
|
|
72
|
+
expect(htmlToWA('<strike>cancelado</strike>')).toBe('~cancelado~')
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('converte o <b> e o <i> do navegador junto com as nossas tags', () => {
|
|
76
|
+
expect(htmlToWA('<b>oi</b> <i>tudo</i> <strong>bem</strong>')).toBe('*oi* _tudo_ *bem*')
|
|
77
|
+
})
|
|
78
|
+
})
|