@adatechnology/conversations-ui 0.1.0-rc.14 → 0.1.0-rc.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-SOWV4264.js → chunk-RNCZO2FM.js} +12 -4
- package/dist/index.js +6 -5
- package/dist/preview/index.js +1 -1
- package/package.json +1 -1
- package/src/AudioRecorderButton.tsx +9 -2
- package/src/MediaRenderer.tsx +21 -2
- package/src/RichMessageComposer.tsx +5 -4
- package/src/documentTypeLabel.test.ts +57 -0
|
@@ -276,6 +276,14 @@ function formatFileSize(bytes) {
|
|
|
276
276
|
// src/MediaRenderer.tsx
|
|
277
277
|
import { useState as useState2 } from "react";
|
|
278
278
|
import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
279
|
+
function documentTypeLabel(filename, mimeType) {
|
|
280
|
+
const extension = filename?.split(".").pop();
|
|
281
|
+
if (extension && extension.length <= 5 && extension !== filename) return extension.toUpperCase();
|
|
282
|
+
const subtype = mimeType?.split(";")[0]?.split("/")[1];
|
|
283
|
+
if (!subtype) return "FILE";
|
|
284
|
+
const compacto = subtype.split(".").pop() ?? subtype;
|
|
285
|
+
return compacto.slice(0, 12).toUpperCase();
|
|
286
|
+
}
|
|
279
287
|
function resolveMediaSource(message) {
|
|
280
288
|
if (message.mediaUrl) return message.mediaUrl;
|
|
281
289
|
if (message.base64) {
|
|
@@ -342,13 +350,13 @@ function MediaRenderer({ message, onLightbox, onResolveUrl, className }) {
|
|
|
342
350
|
return /* @__PURE__ */ jsx5("div", { className: "min-w-[200px]", children: src ? /* @__PURE__ */ jsx5(AudioPlayer, { src, isMine: message.direction === "outbound" }) : /* @__PURE__ */ jsx5("div", { className: "h-12 bg-gray-200 rounded-lg flex items-center justify-center text-gray-400 text-xs", children: bubble.mediaUnavailable }) });
|
|
343
351
|
}
|
|
344
352
|
case "document": {
|
|
345
|
-
const typeLabel = message.mimeType
|
|
353
|
+
const typeLabel = documentTypeLabel(message.filename, message.mimeType);
|
|
346
354
|
const sizeLabel = message.sizeBytes ? formatFileSize(message.sizeBytes) : null;
|
|
347
355
|
return /* @__PURE__ */ jsxs3("div", { className: cn("flex items-center gap-3 min-w-[200px]", className), children: [
|
|
348
356
|
/* @__PURE__ */ jsx5("div", { className: "w-10 h-10 bg-gray-200 rounded-lg flex items-center justify-center flex-shrink-0", children: /* @__PURE__ */ jsx5(FileIcon, { filename: message.filename, mimeType: message.mimeType }) }),
|
|
349
357
|
/* @__PURE__ */ jsxs3("div", { className: "flex-1 min-w-0", children: [
|
|
350
358
|
/* @__PURE__ */ jsx5("p", { className: "text-sm font-medium truncate", children: message.filename ?? bubble.untitledDocument }),
|
|
351
|
-
/* @__PURE__ */ jsx5("p", { className: "text-xs text-gray-500", children: sizeLabel ? `${typeLabel} \xB7 ${sizeLabel}` : typeLabel })
|
|
359
|
+
/* @__PURE__ */ jsx5("p", { className: "truncate text-xs text-gray-500", children: sizeLabel ? `${typeLabel} \xB7 ${sizeLabel}` : typeLabel })
|
|
352
360
|
] }),
|
|
353
361
|
src ? /* @__PURE__ */ jsx5("a", { href: src, download: message.filename, className: "w-8 h-8 flex items-center justify-center rounded-full bg-gray-200 hover:bg-gray-300 flex-shrink-0 transition-colors", "aria-label": bubble.downloadFile, children: /* @__PURE__ */ jsxs3("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", className: "text-gray-600", children: [
|
|
354
362
|
/* @__PURE__ */ jsx5("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }),
|
|
@@ -1187,9 +1195,9 @@ function AudioRecorderButton({
|
|
|
1187
1195
|
{
|
|
1188
1196
|
role: "group",
|
|
1189
1197
|
"aria-label": labelOf("review"),
|
|
1190
|
-
className: "absolute bottom-full right-0 z-20 mb-2 flex w-
|
|
1198
|
+
className: "absolute bottom-full right-0 z-20 mb-2 flex w-[calc(100vw-2rem)] max-w-[20rem] flex-wrap items-center justify-end gap-2 rounded-xl border border-gray-200 bg-white p-2 shadow-lg dark:border-gray-700 dark:bg-gray-800",
|
|
1191
1199
|
children: [
|
|
1192
|
-
/* @__PURE__ */ jsx13("
|
|
1200
|
+
/* @__PURE__ */ jsx13("div", { className: "min-w-0 flex-1", children: /* @__PURE__ */ jsx13(AudioPlayer, { src: pending.objectURL }) }),
|
|
1193
1201
|
/* @__PURE__ */ jsx13(
|
|
1194
1202
|
"button",
|
|
1195
1203
|
{
|
package/dist/index.js
CHANGED
|
@@ -44,7 +44,7 @@ import {
|
|
|
44
44
|
useConversationDocuments,
|
|
45
45
|
useConversationLocales,
|
|
46
46
|
useConversations
|
|
47
|
-
} from "./chunk-
|
|
47
|
+
} from "./chunk-RNCZO2FM.js";
|
|
48
48
|
import {
|
|
49
49
|
htmlToWA,
|
|
50
50
|
parseWhatsAppFormatting,
|
|
@@ -319,15 +319,16 @@ var RichMessageComposer = forwardRef(function RichMessageComposer2({
|
|
|
319
319
|
return /* @__PURE__ */ jsxs2("div", { className: cn("flex flex-col", className), children: [
|
|
320
320
|
attachmentsPreview,
|
|
321
321
|
quickReplies?.length ? (
|
|
322
|
-
/* Uma linha
|
|
323
|
-
|
|
324
|
-
|
|
322
|
+
/* Uma linha só, rolando na horizontal. Deixar quebrar em várias linhas faz a barra crescer
|
|
323
|
+
conforme o número de respostas rápidas e empurrar a conversa para cima — o composer
|
|
324
|
+
precisa ter altura previsível, independente de quantos atalhos o host configurou. */
|
|
325
|
+
/* @__PURE__ */ jsx2("div", { className: "mb-2 flex flex-nowrap gap-1 overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden", children: quickReplies.map((quickReply) => /* @__PURE__ */ jsx2(
|
|
325
326
|
"button",
|
|
326
327
|
{
|
|
327
328
|
type: "button",
|
|
328
329
|
title: quickReply.tooltip,
|
|
329
330
|
onClick: () => replaceContent(quickReply.text),
|
|
330
|
-
className: "whitespace-nowrap rounded-full border border-gray-200 bg-white px-2 py-1 text-xs text-gray-600 transition-colors hover:border-teal-200 hover:bg-teal-50 hover:text-teal-700 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400 dark:hover:border-teal-800 dark:hover:bg-teal-950/40 dark:hover:text-teal-400",
|
|
331
|
+
className: "flex-shrink-0 whitespace-nowrap rounded-full border border-gray-200 bg-white px-2 py-1 text-xs text-gray-600 transition-colors hover:border-teal-200 hover:bg-teal-50 hover:text-teal-700 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400 dark:hover:border-teal-800 dark:hover:bg-teal-950/40 dark:hover:text-teal-400",
|
|
331
332
|
children: quickReply.label
|
|
332
333
|
},
|
|
333
334
|
quickReply.id
|
package/dist/preview/index.js
CHANGED
package/package.json
CHANGED
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
|
|
11
11
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
12
12
|
|
|
13
|
+
import { AudioPlayer } from './AudioPlayer'
|
|
14
|
+
|
|
13
15
|
export interface AudioRecorderButtonLabels {
|
|
14
16
|
start: string
|
|
15
17
|
stop: string
|
|
@@ -192,9 +194,14 @@ export function AudioRecorderButton({
|
|
|
192
194
|
<div
|
|
193
195
|
role="group"
|
|
194
196
|
aria-label={labelOf('review')}
|
|
195
|
-
|
|
197
|
+
/* Largura presa à viewport, não ao conteúdo: ancorado à direita do microfone, qualquer
|
|
198
|
+
largura fixa maior que a tela sangra para fora no celular. `flex-wrap` é a segunda
|
|
199
|
+
rede — se o player e os dois botões não couberem lado a lado, os botões descem. */
|
|
200
|
+
className="absolute bottom-full right-0 z-20 mb-2 flex w-[calc(100vw-2rem)] max-w-[20rem] flex-wrap items-center justify-end gap-2 rounded-xl border border-gray-200 bg-white p-2 shadow-lg dark:border-gray-700 dark:bg-gray-800"
|
|
196
201
|
>
|
|
197
|
-
<
|
|
202
|
+
<div className="min-w-0 flex-1">
|
|
203
|
+
<AudioPlayer src={pending.objectURL} />
|
|
204
|
+
</div>
|
|
198
205
|
<button
|
|
199
206
|
type="button"
|
|
200
207
|
onClick={discard}
|
package/src/MediaRenderer.tsx
CHANGED
|
@@ -6,6 +6,25 @@ import { formatFileSize } from './lib/format'
|
|
|
6
6
|
import { cn } from './lib/cn'
|
|
7
7
|
import type { MessagePayload } from './types'
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Rótulo curto do tipo, para a linha de baixo da bolha de documento.
|
|
11
|
+
*
|
|
12
|
+
* O subtipo cru do Office é gigantesco — `vnd.openxmlformats-officedocument.wordprocessingml.document`
|
|
13
|
+
* vira uma linha de 58 caracteres em caixa alta que estica a bolha, empurra a coluna da conversa e
|
|
14
|
+
* quebra o layout de três painéis. A extensão do arquivo diz a mesma coisa em quatro letras.
|
|
15
|
+
*/
|
|
16
|
+
export function documentTypeLabel(filename?: string, mimeType?: string): string {
|
|
17
|
+
const extension = filename?.split('.').pop()
|
|
18
|
+
if (extension && extension.length <= 5 && extension !== filename) return extension.toUpperCase()
|
|
19
|
+
|
|
20
|
+
const subtype = mimeType?.split(';')[0]?.split('/')[1]
|
|
21
|
+
if (!subtype) return 'FILE'
|
|
22
|
+
// Sem extensão e com subtipo longo (áudio/vídeo sem nome), fica o sufixo depois do último ponto:
|
|
23
|
+
// `…wordprocessingml.document` -> `DOCUMENT`, que ainda informa e não estoura.
|
|
24
|
+
const compacto = subtype.split('.').pop() ?? subtype
|
|
25
|
+
return compacto.slice(0, 12).toUpperCase()
|
|
26
|
+
}
|
|
27
|
+
|
|
9
28
|
function resolveMediaSource(message: MessagePayload): string | null {
|
|
10
29
|
if (message.mediaUrl) return message.mediaUrl
|
|
11
30
|
if (message.base64) {
|
|
@@ -126,7 +145,7 @@ export function MediaRenderer({ message, onLightbox, onResolveUrl, className }:
|
|
|
126
145
|
)
|
|
127
146
|
}
|
|
128
147
|
case 'document': {
|
|
129
|
-
const typeLabel = message.mimeType
|
|
148
|
+
const typeLabel = documentTypeLabel(message.filename, message.mimeType)
|
|
130
149
|
const sizeLabel = message.sizeBytes ? formatFileSize(message.sizeBytes) : null
|
|
131
150
|
return (
|
|
132
151
|
<div className={cn('flex items-center gap-3 min-w-[200px]', className)}>
|
|
@@ -135,7 +154,7 @@ export function MediaRenderer({ message, onLightbox, onResolveUrl, className }:
|
|
|
135
154
|
</div>
|
|
136
155
|
<div className="flex-1 min-w-0">
|
|
137
156
|
<p className="text-sm font-medium truncate">{message.filename ?? bubble.untitledDocument}</p>
|
|
138
|
-
<p className="text-xs text-gray-500">{sizeLabel ? `${typeLabel} · ${sizeLabel}` : typeLabel}</p>
|
|
157
|
+
<p className="truncate text-xs text-gray-500">{sizeLabel ? `${typeLabel} · ${sizeLabel}` : typeLabel}</p>
|
|
139
158
|
</div>
|
|
140
159
|
{src ? (
|
|
141
160
|
<a href={src} download={message.filename} className="w-8 h-8 flex items-center justify-center rounded-full bg-gray-200 hover:bg-gray-300 flex-shrink-0 transition-colors" aria-label={bubble.downloadFile}>
|
|
@@ -246,16 +246,17 @@ export const RichMessageComposer = forwardRef<RichMessageComposerHandle, RichMes
|
|
|
246
246
|
{attachmentsPreview}
|
|
247
247
|
|
|
248
248
|
{quickReplies?.length ? (
|
|
249
|
-
/* Uma linha
|
|
250
|
-
|
|
251
|
-
|
|
249
|
+
/* Uma linha só, rolando na horizontal. Deixar quebrar em várias linhas faz a barra crescer
|
|
250
|
+
conforme o número de respostas rápidas e empurrar a conversa para cima — o composer
|
|
251
|
+
precisa ter altura previsível, independente de quantos atalhos o host configurou. */
|
|
252
|
+
<div className="mb-2 flex flex-nowrap gap-1 overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
|
|
252
253
|
{quickReplies.map((quickReply) => (
|
|
253
254
|
<button
|
|
254
255
|
key={quickReply.id}
|
|
255
256
|
type="button"
|
|
256
257
|
title={quickReply.tooltip}
|
|
257
258
|
onClick={() => replaceContent(quickReply.text)}
|
|
258
|
-
className="whitespace-nowrap rounded-full border border-gray-200 bg-white px-2 py-1 text-xs text-gray-600 transition-colors hover:border-teal-200 hover:bg-teal-50 hover:text-teal-700 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400 dark:hover:border-teal-800 dark:hover:bg-teal-950/40 dark:hover:text-teal-400"
|
|
259
|
+
className="flex-shrink-0 whitespace-nowrap rounded-full border border-gray-200 bg-white px-2 py-1 text-xs text-gray-600 transition-colors hover:border-teal-200 hover:bg-teal-50 hover:text-teal-700 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400 dark:hover:border-teal-800 dark:hover:bg-teal-950/40 dark:hover:text-teal-400"
|
|
259
260
|
>
|
|
260
261
|
{quickReply.label}
|
|
261
262
|
</button>
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guarda o rótulo de tipo da bolha de documento.
|
|
3
|
+
*
|
|
4
|
+
* O defeito que motivou: `mimeType.split('/')[1].toUpperCase()` transformava um `.docx` em
|
|
5
|
+
* `VND.OPENXMLFORMATS-OFFICEDOCUMENT.WORDPROCESSINGML.DOCUMENT` — 58 caracteres numa linha que não
|
|
6
|
+
* quebra, esticando a bolha até empurrar a coluna da conversa e estourar o layout de três painéis.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, expect, it } from 'bun:test'
|
|
10
|
+
|
|
11
|
+
import { documentTypeLabel } from './MediaRenderer'
|
|
12
|
+
|
|
13
|
+
describe('documentTypeLabel', () => {
|
|
14
|
+
// O caso que quebrou a tela.
|
|
15
|
+
it('usa a extensão do arquivo em vez do subtipo gigante do Office', () => {
|
|
16
|
+
const rotulo = documentTypeLabel(
|
|
17
|
+
'contrato.docx',
|
|
18
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
expect(rotulo).toBe('DOCX')
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('serve para os tipos curtos também', () => {
|
|
25
|
+
expect(documentTypeLabel('nota-fiscal.pdf', 'application/pdf')).toBe('PDF')
|
|
26
|
+
expect(documentTypeLabel('lista.txt', 'text/plain')).toBe('TXT')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
// Áudio e sticker chegam sem nome de arquivo: o rótulo vem do mimeType, ainda curto.
|
|
30
|
+
it('cai no sufixo do subtipo quando não há nome', () => {
|
|
31
|
+
expect(documentTypeLabel(undefined, 'application/vnd.ms-excel')).toBe('MS-EXCEL')
|
|
32
|
+
expect(documentTypeLabel(undefined, 'application/pdf')).toBe('PDF')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('descarta parâmetro do mimeType', () => {
|
|
36
|
+
expect(documentTypeLabel(undefined, 'audio/ogg; codecs=opus')).toBe('OGG')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
// Nome sem ponto não tem extensão — o id da mídia, por exemplo.
|
|
40
|
+
it('não confunde nome sem extensão com extensão', () => {
|
|
41
|
+
expect(documentTypeLabel('seed-media-03', 'application/zip')).toBe('ZIP')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('tem rótulo para o caso sem nome e sem tipo', () => {
|
|
45
|
+
expect(documentTypeLabel()).toBe('FILE')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
// Teto de tamanho: nenhum rótulo pode voltar a esticar a bolha.
|
|
49
|
+
it('nunca passa de 12 caracteres', () => {
|
|
50
|
+
const longo = documentTypeLabel(
|
|
51
|
+
undefined,
|
|
52
|
+
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
expect(longo.length).toBeLessThanOrEqual(12)
|
|
56
|
+
})
|
|
57
|
+
})
|