@adatechnology/conversations-ui 0.3.2 → 0.4.1

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.
@@ -17,6 +17,7 @@ export const CONVERSATION_CHANNEL = {
17
17
  MESSENGER: 'messenger',
18
18
  INSTAGRAM: 'instagram',
19
19
  WEBCHAT: 'webchat',
20
+ EMAIL: 'email',
20
21
  } as const
21
22
  export type ConversationChannel = (typeof CONVERSATION_CHANNEL)[keyof typeof CONVERSATION_CHANNEL]
22
23
 
@@ -35,14 +36,21 @@ export const HANDLE_KIND = {
35
36
  PHONE: 'phone',
36
37
  USERNAME: 'username',
37
38
  SESSION: 'session',
39
+ EMAIL: 'email',
38
40
  } as const
39
41
  export type HandleKind = (typeof HANDLE_KIND)[keyof typeof HANDLE_KIND]
40
42
 
43
+ /**
44
+ * Só o que é **exibição** do canal — rótulo, ícone, como reabrir, como mostrar o identificador.
45
+ * Capacidade de **comportamento** (confirma leitura, tem janela e de quantas horas, aceita anexo e
46
+ * áudio, precisa de transporte próprio) não mora mais aqui: para canal que
47
+ * `@adatechnology/conversation-contracts` conhece, ela vem de `channelCapabilityFor` (RF10). Manter
48
+ * os dois tipos de capacidade juntos foi o que criou a "janela" duplicada (`windowHours`) que a
49
+ * spec 211 mandou apagar — a mesma verdade não pode morar em dois lugares.
50
+ */
41
51
  export type ChannelCapabilities = {
42
52
  readonly label: string
43
53
  readonly icon: string
44
- readonly hasSessionWindow: boolean
45
- readonly windowHours: number
46
54
  readonly reopenMechanism: ReopenMechanism
47
55
  readonly handleKind: HandleKind
48
56
  }
@@ -51,8 +59,6 @@ export const CHANNEL_CAPABILITIES: Readonly<Record<ConversationChannel, ChannelC
51
59
  [CONVERSATION_CHANNEL.WHATSAPP]: {
52
60
  label: 'WhatsApp',
53
61
  icon: '💬',
54
- hasSessionWindow: true,
55
- windowHours: 24,
56
62
  reopenMechanism: REOPEN_MECHANISM.TEMPLATE,
57
63
  handleKind: HANDLE_KIND.PHONE,
58
64
  },
@@ -60,16 +66,12 @@ export const CHANNEL_CAPABILITIES: Readonly<Record<ConversationChannel, ChannelC
60
66
  // Messenger também tem 24h, mas reabre com message tag — não com template aprovado.
61
67
  label: 'Messenger',
62
68
  icon: '📨',
63
- hasSessionWindow: true,
64
- windowHours: 24,
65
69
  reopenMechanism: REOPEN_MECHANISM.TAG,
66
70
  handleKind: HANDLE_KIND.USERNAME,
67
71
  },
68
72
  [CONVERSATION_CHANNEL.INSTAGRAM]: {
69
73
  label: 'Instagram',
70
74
  icon: '📷',
71
- hasSessionWindow: true,
72
- windowHours: 24,
73
75
  reopenMechanism: REOPEN_MECHANISM.TAG,
74
76
  handleKind: HANDLE_KIND.USERNAME,
75
77
  },
@@ -77,11 +79,16 @@ export const CHANNEL_CAPABILITIES: Readonly<Record<ConversationChannel, ChannelC
77
79
  // Chat próprio: sem intermediário, sem janela. Bloquear o composer aqui seria inventar limite.
78
80
  label: 'Chat do site',
79
81
  icon: '🌐',
80
- hasSessionWindow: false,
81
- windowHours: 0,
82
82
  reopenMechanism: REOPEN_MECHANISM.NONE,
83
83
  handleKind: HANDLE_KIND.SESSION,
84
84
  },
85
+ [CONVERSATION_CHANNEL.EMAIL]: {
86
+ // RF10: e-mail entra no vocabulário; ele não reabre por template nem por tag — é só resposta.
87
+ label: 'E-mail',
88
+ icon: '✉️',
89
+ reopenMechanism: REOPEN_MECHANISM.NONE,
90
+ handleKind: HANDLE_KIND.EMAIL,
91
+ },
85
92
  }
86
93
 
87
94
  export function capabilitiesOf(channel: ConversationChannel | undefined): ChannelCapabilities {
@@ -135,6 +142,8 @@ export function formatContactHandle(params: FormatContactHandleParams): string {
135
142
 
136
143
  if (handleKind === HANDLE_KIND.PHONE) return formatPhone(params.handle)
137
144
  if (handleKind === HANDLE_KIND.USERNAME) return params.handle.startsWith('@') ? params.handle : `@${params.handle}`
145
+ // Endereço de e-mail já é o identificador exibível — nem telefone, nem "@usuário".
146
+ if (handleKind === HANDLE_KIND.EMAIL) return params.handle
138
147
 
139
148
  // Sessão de chat de site é um id opaco: mostrar o hash inteiro não ajuda ninguém.
140
149
  return `Visitante ${params.handle.slice(-6)}`
@@ -1,13 +1,15 @@
1
1
  /**
2
2
  * Janela de sessão: o intervalo em que o canal aceita mensagem livre do atendente. No WhatsApp são
3
3
  * 24h desde o último contato do cliente; fora dela só template. Cada canal tem a sua regra — e há
4
- * canal sem janela nenhuma — então a política vem de `capabilitiesOf`, não de constante fixa.
4
+ * canal sem janela nenhuma — então a política vem de `channelCapabilityFor` (RF10), não de
5
+ * constante fixa.
5
6
  *
6
7
  * Mora no SDK porque é regra de plataforma, não de produto: todo projeto que usa este pacote
7
8
  * precisa dela para saber o que o atendente ainda consegue fazer.
8
9
  */
9
10
 
10
- import { capabilitiesOf, type ConversationChannel } from './conversationChannel'
11
+ import { channelCapabilityFor } from './channelCapability'
12
+ import type { ConversationChannel } from './conversationChannel'
11
13
 
12
14
  const MINUTE_MS = 60_000
13
15
  const HOUR_MS = 60 * MINUTE_MS
@@ -52,12 +54,12 @@ export type WindowOfParams = {
52
54
  * bloquearia o composer inventando um limite que a plataforma não impõe.
53
55
  */
54
56
  export function windowOf(params: WindowOfParams): ConversationWindow {
55
- const capabilities = capabilitiesOf(params.channel)
56
- if (!capabilities.hasSessionWindow) return CONVERSATION_WINDOW.FRESH
57
+ const capability = channelCapabilityFor(params.channel)
58
+ if (capability.sessionWindowHours === null) return CONVERSATION_WINDOW.FRESH
57
59
 
58
60
  if (!params.lastInboundAt) return CONVERSATION_WINDOW.EXPIRED
59
61
 
60
- const windowMs = capabilities.windowHours * HOUR_MS
62
+ const windowMs = capability.sessionWindowHours * HOUR_MS
61
63
  const elapsed = params.now - new Date(params.lastInboundAt).getTime()
62
64
 
63
65
  if (elapsed >= windowMs) return CONVERSATION_WINDOW.EXPIRED
package/src/index.ts CHANGED
@@ -53,7 +53,7 @@ export {
53
53
  isReplyOverdue,
54
54
  } from './replyLatency'
55
55
  export type { ReplyLatency, ReplyLatencyParams } from './replyLatency'
56
- // Canal de origem: capacidades por plataforma (janela de sessão, reabertura, tipo de identificador).
56
+ // Canal de origem: exibição por plataforma (rótulo, ícone, reabertura, tipo de identificador).
57
57
  export {
58
58
  CONVERSATION_CHANNEL,
59
59
  DEFAULT_CONVERSATION_CHANNEL,
@@ -75,6 +75,10 @@ export type {
75
75
  HandleKind,
76
76
  FormatContactHandleParams,
77
77
  } from './conversationChannel'
78
+ // Capacidade comportamental do canal (RF10, D3) — confirma leitura, janela, anexo, áudio, resposta
79
+ // rápida, transporte próprio. Vem de `@adatechnology/conversation-contracts`; ver channelCapability.ts.
80
+ export { channelCapabilityFor } from './channelCapability'
81
+ export type { ChannelCapability } from '@adatechnology/conversation-contracts'
78
82
  export type { ConversationWindow } from './conversationWindow'
79
83
  export { ConversationRow } from './ConversationRow'
80
84
  export { ChannelIcon, CHANNEL_BRAND_COLOR } from './ChannelIcon'
@@ -66,6 +66,10 @@ const SIMULATOR_PANEL_CHANNEL_WORDING: Readonly<Record<ConversationChannel, Chan
66
66
  destinationHint: 'entrega na API do chat do site',
67
67
  placeholder: 'Escreva como o visitante…',
68
68
  },
69
+ [CONVERSATION_CHANNEL.EMAIL]: {
70
+ destinationHint: 'entrega na caixa de entrada do contratante',
71
+ placeholder: 'Escreva como quem respondeu o e-mail…',
72
+ },
69
73
  }
70
74
 
71
75
  /** Rótulos do painel já resolvidos para o canal — útil para o host que monta o cabeçalho por fora. */
@@ -389,6 +389,7 @@ export function ConversationPane({
389
389
  <MessageBubble
390
390
  message={message}
391
391
  isMine={message.direction === 'outbound'}
392
+ channel={conversation.channel}
392
393
  isFirstInGroup={!previous || previous.sender !== message.sender}
393
394
  {...(messageSelection
394
395
  ? {
@@ -507,6 +508,7 @@ export function ConversationPane({
507
508
  value={draft}
508
509
  onChange={setDraft}
509
510
  onSend={(text) => void handleSend(text)}
511
+ channel={conversation.channel}
510
512
  // Habilita clipe E microfone: o composer desenha o gravador sozinho quando existe um jeito
511
513
  // de entregar arquivo, porque áudio gravado é um anexo como qualquer outro.
512
514
  {...(onAttach ? { onAttach: (file: File) => void handleAttach(file) } : {})}