@adatechnology/conversations-ui 0.10.0 → 0.11.0
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/README.md +1 -0
- package/dist/participant/index.d.ts +4 -0
- package/dist/participant/index.js +8 -2
- package/dist/styles.css +5 -0
- package/package.json +1 -1
- package/src/participant/ConversationThread.tsx +3 -0
- package/src/participant/ParticipantConversations.tsx +2 -0
- package/src/participant/ParticipantThread.test.tsx +46 -0
- package/src/participant/ParticipantThread.tsx +10 -0
- package/src/participant/ParticipantThreadScreen.tsx +1 -0
- package/src/styles.css +1 -0
package/README.md
CHANGED
|
@@ -334,6 +334,7 @@ import { ConversationThread } from '@adatechnology/conversations-ui/participant'
|
|
|
334
334
|
| `status?` | `'open' \| 'closed'` | `closed` tira o compositor e mostra `labels.closedNotice` |
|
|
335
335
|
| `counterpartLabel?` | `string` | linha curta sob o título, montada pelo host |
|
|
336
336
|
| `headerActions?` | `ReactNode` | slot à direita do cabeçalho; ausente não desenha nada. Espera botões curtos ou só ícone, cada um com `aria-label`: com ações, a barra quebra de linha e o título mantém no mínimo `6rem` |
|
|
337
|
+
| `renderComposerLock?` | `(conversation) => ReactNode \| undefined` | devolver um nó troca o compositor por ele (aviso + ações do host), dentro de `cv-p-thread__lock`; `undefined` mantém o compositor. `closed` continua tendo prioridade. Avaliado a cada render; o rascunho fica no controlador e volta ao destravar. Também existe em `ParticipantConversations` |
|
|
337
338
|
| `onBack?` | `() => void` | ausente = sem seta (o diálogo do host já tem o próprio fechar) |
|
|
338
339
|
| `quickReplies?` | `QuickReply[]` | chips acima do campo; tocar **preenche**, nunca envia |
|
|
339
340
|
| `renderSubjectIcon?`, `subjectGroups?` | | ícone do assunto no cabeçalho, como na lista |
|
|
@@ -244,6 +244,8 @@ type ParticipantConversationsProps = {
|
|
|
244
244
|
tail?: boolean;
|
|
245
245
|
/** Absent draws no microphone. Present lets the user record and send an audio as a message of its own. */
|
|
246
246
|
audioRecording?: ParticipantAudioRecordingOptions;
|
|
247
|
+
/** Return a node to replace the composer for this conversation (notice + host actions); undefined keeps the composer. `closed` still wins. Evaluated every render. */
|
|
248
|
+
renderComposerLock?: (conversation: ParticipantConversationSummary) => ReactNode | undefined;
|
|
247
249
|
};
|
|
248
250
|
declare function ParticipantConversations(props: ParticipantConversationsProps): react.JSX.Element;
|
|
249
251
|
|
|
@@ -287,6 +289,8 @@ type ConversationThreadProps = {
|
|
|
287
289
|
readonly renderAuthorAvatar?: ParticipantAuthorAvatarRenderer;
|
|
288
290
|
/** Absent draws no microphone. Present lets the user record and send an audio as a message of its own. */
|
|
289
291
|
readonly audioRecording?: ParticipantAudioRecordingOptions;
|
|
292
|
+
/** Return a node to replace the composer (notice + host actions); undefined keeps the composer. `closed` still wins. Evaluated every render. */
|
|
293
|
+
readonly renderComposerLock?: (conversation: ParticipantConversationSummary) => ReactNode | undefined;
|
|
290
294
|
};
|
|
291
295
|
/** One conversation about one subject, drawn like the participant app and seen from either side. */
|
|
292
296
|
declare function ConversationThread(props: ConversationThreadProps): react.JSX.Element;
|
|
@@ -2908,6 +2908,10 @@ function Footer({ props }) {
|
|
|
2908
2908
|
if (conversation.status === "closed") {
|
|
2909
2909
|
return /* @__PURE__ */ jsx33("p", { className: "cv-p-thread__closed", children: labels.closedNotice });
|
|
2910
2910
|
}
|
|
2911
|
+
const lock = props.renderComposerLock?.(conversation);
|
|
2912
|
+
if (lock !== void 0 && lock !== null) {
|
|
2913
|
+
return /* @__PURE__ */ jsx33("div", { className: "cv-p-thread__lock", role: "region", "aria-live": "polite", children: lock });
|
|
2914
|
+
}
|
|
2911
2915
|
return /* @__PURE__ */ jsx33(
|
|
2912
2916
|
ParticipantComposer,
|
|
2913
2917
|
{
|
|
@@ -3675,7 +3679,8 @@ function LoadedThread(props) {
|
|
|
3675
3679
|
avatars: props.avatars,
|
|
3676
3680
|
renderAuthorAvatar: props.renderAuthorAvatar,
|
|
3677
3681
|
tail: props.tail,
|
|
3678
|
-
audioRecording: props.audioRecording
|
|
3682
|
+
audioRecording: props.audioRecording,
|
|
3683
|
+
renderComposerLock: props.renderComposerLock
|
|
3679
3684
|
}
|
|
3680
3685
|
);
|
|
3681
3686
|
}
|
|
@@ -4043,7 +4048,8 @@ function ConversationThreadBody(props) {
|
|
|
4043
4048
|
tail: props.tail,
|
|
4044
4049
|
counterpartLabel: props.counterpartLabel,
|
|
4045
4050
|
headerActions: props.headerActions,
|
|
4046
|
-
audioRecording: props.audioRecording
|
|
4051
|
+
audioRecording: props.audioRecording,
|
|
4052
|
+
renderComposerLock: props.renderComposerLock
|
|
4047
4053
|
}
|
|
4048
4054
|
) })
|
|
4049
4055
|
}
|
package/dist/styles.css
CHANGED
|
@@ -1812,6 +1812,11 @@
|
|
|
1812
1812
|
white-space: nowrap;
|
|
1813
1813
|
border: 0;
|
|
1814
1814
|
}
|
|
1815
|
+
.cv-p-thread__lock {
|
|
1816
|
+
padding: 1rem;
|
|
1817
|
+
color: var(--cv-p-i-text-muted);
|
|
1818
|
+
border-top: 1px solid var(--cv-p-i-border);
|
|
1819
|
+
}
|
|
1815
1820
|
.cv-p-thread__closed {
|
|
1816
1821
|
margin: 0;
|
|
1817
1822
|
padding: 1rem;
|
package/package.json
CHANGED
|
@@ -53,6 +53,8 @@ export type ConversationThreadProps = {
|
|
|
53
53
|
readonly renderAuthorAvatar?: ParticipantAuthorAvatarRenderer
|
|
54
54
|
/** Absent draws no microphone. Present lets the user record and send an audio as a message of its own. */
|
|
55
55
|
readonly audioRecording?: ParticipantAudioRecordingOptions
|
|
56
|
+
/** Return a node to replace the composer (notice + host actions); undefined keeps the composer. `closed` still wins. Evaluated every render. */
|
|
57
|
+
readonly renderComposerLock?: (conversation: ParticipantConversationSummary) => ReactNode | undefined
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
function useConversationThreadCore(props: ConversationThreadProps): ParticipantThreadCoreProps {
|
|
@@ -109,6 +111,7 @@ function ConversationThreadBody(props: ConversationThreadProps) {
|
|
|
109
111
|
counterpartLabel={props.counterpartLabel}
|
|
110
112
|
headerActions={props.headerActions}
|
|
111
113
|
audioRecording={props.audioRecording}
|
|
114
|
+
renderComposerLock={props.renderComposerLock}
|
|
112
115
|
/>
|
|
113
116
|
</div>
|
|
114
117
|
</div>
|
|
@@ -44,6 +44,8 @@ export type ParticipantConversationsProps = {
|
|
|
44
44
|
tail?: boolean
|
|
45
45
|
/** Absent draws no microphone. Present lets the user record and send an audio as a message of its own. */
|
|
46
46
|
audioRecording?: ParticipantAudioRecordingOptions
|
|
47
|
+
/** Return a node to replace the composer for this conversation (notice + host actions); undefined keeps the composer. `closed` still wins. Evaluated every render. */
|
|
48
|
+
renderComposerLock?: (conversation: ParticipantConversationSummary) => ReactNode | undefined
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
export function ParticipantConversations(props: ParticipantConversationsProps) {
|
|
@@ -128,6 +128,52 @@ describe('ParticipantThread', () => {
|
|
|
128
128
|
expect(markup).not.toContain('<textarea')
|
|
129
129
|
})
|
|
130
130
|
|
|
131
|
+
it('replaces the composer with the lock node inside the lock region', () => {
|
|
132
|
+
const markup = render({ renderComposerLock: () => <p>Assumed by Ana</p> })
|
|
133
|
+
|
|
134
|
+
expect(markup).toContain(
|
|
135
|
+
'<div class="cv-p-thread__lock" role="region" aria-live="polite"><p>Assumed by Ana</p></div>',
|
|
136
|
+
)
|
|
137
|
+
expect(markup).not.toContain('<form')
|
|
138
|
+
expect(markup).not.toContain('<textarea')
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('keeps the composer when renderComposerLock returns undefined or null', () => {
|
|
142
|
+
expect(render({ renderComposerLock: () => undefined })).toContain('<form')
|
|
143
|
+
expect(render({ renderComposerLock: () => null })).toContain('<form')
|
|
144
|
+
expect(render({ renderComposerLock: () => undefined })).not.toContain('cv-p-thread__lock')
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('lets the closed notice win over the lock', () => {
|
|
148
|
+
const markup = render({
|
|
149
|
+
conversation: buildConversation({ subjectId: '1', status: 'closed' }),
|
|
150
|
+
renderComposerLock: () => <p>Assumed by Ana</p>,
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
expect(markup).toContain('This conversation is closed')
|
|
154
|
+
expect(markup).not.toContain('Assumed by Ana')
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('passes the conversation to renderComposerLock and brings the draft back when it unlocks', () => {
|
|
158
|
+
const draft = { value: 'half typed', onChange: () => undefined, files: [], onFilesChange: () => undefined }
|
|
159
|
+
const conversation = buildConversation({ subjectId: '7' })
|
|
160
|
+
const seen: string[] = []
|
|
161
|
+
|
|
162
|
+
const locked = render({
|
|
163
|
+
draft,
|
|
164
|
+
conversation,
|
|
165
|
+
renderComposerLock: (received) => {
|
|
166
|
+
seen.push(received.subjectId)
|
|
167
|
+
return <p>Locked</p>
|
|
168
|
+
},
|
|
169
|
+
})
|
|
170
|
+
const unlocked = render({ draft, conversation, renderComposerLock: () => undefined })
|
|
171
|
+
|
|
172
|
+
expect(seen).toEqual(['7'])
|
|
173
|
+
expect(locked).not.toContain('half typed')
|
|
174
|
+
expect(unlocked).toContain('half typed')
|
|
175
|
+
})
|
|
176
|
+
|
|
131
177
|
it('shows the composer when the conversation is open', () => {
|
|
132
178
|
const markup = render()
|
|
133
179
|
|
|
@@ -76,6 +76,8 @@ export type ParticipantThreadProps = {
|
|
|
76
76
|
readonly audioRecording?: ParticipantAudioRecordingOptions
|
|
77
77
|
/** Receives the recorded audio; the controller sends it as a message of its own. */
|
|
78
78
|
readonly onSendAudio?: (file: File) => void
|
|
79
|
+
/** Return a node to replace the composer for this conversation (notice + host actions); undefined keeps the composer. Evaluated every render. */
|
|
80
|
+
readonly renderComposerLock?: (conversation: ParticipantConversationSummary) => ReactNode | undefined
|
|
79
81
|
}
|
|
80
82
|
|
|
81
83
|
function Footer({ props }: { readonly props: ParticipantThreadProps }) {
|
|
@@ -83,6 +85,14 @@ function Footer({ props }: { readonly props: ParticipantThreadProps }) {
|
|
|
83
85
|
if (conversation.status === 'closed') {
|
|
84
86
|
return <p className="cv-p-thread__closed">{labels.closedNotice}</p>
|
|
85
87
|
}
|
|
88
|
+
const lock = props.renderComposerLock?.(conversation)
|
|
89
|
+
if (lock !== undefined && lock !== null) {
|
|
90
|
+
return (
|
|
91
|
+
<div className="cv-p-thread__lock" role="region" aria-live="polite">
|
|
92
|
+
{lock}
|
|
93
|
+
</div>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
86
96
|
return (
|
|
87
97
|
<ParticipantComposer
|
|
88
98
|
value={draft.value}
|
package/src/styles.css
CHANGED
|
@@ -1275,6 +1275,7 @@
|
|
|
1275
1275
|
.cv-p-thread__messages { display: flex; flex-direction: column; gap: 0.5rem; }
|
|
1276
1276
|
.cv-p-thread__live { padding: 0 1rem; font-size: 0.75rem; color: var(--cv-p-i-accent); background: var(--cv-p-i-surface-raised); }
|
|
1277
1277
|
.cv-p-thread__live:empty { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
|
|
1278
|
+
.cv-p-thread__lock { padding: 1rem; color: var(--cv-p-i-text-muted); border-top: 1px solid var(--cv-p-i-border); }
|
|
1278
1279
|
.cv-p-thread__closed { margin: 0; padding: 1rem; text-align: center; color: var(--cv-p-i-text-muted); border-top: 1px solid var(--cv-p-i-border); }
|
|
1279
1280
|
.cv-p-day { align-self: center; margin: 0.5rem 0; padding: 0.25rem 0.75rem; background: var(--cv-p-i-surface-raised); border: 1px solid var(--cv-p-i-border); border-radius: var(--cv-p-i-radius); font: 0.65rem 'SFMono-Regular', Consolas, monospace; letter-spacing: 0.06em; text-transform: uppercase; color: var(--cv-p-i-text-muted); }
|
|
1280
1281
|
|