@adatechnology/conversations-ui 0.11.0 → 0.12.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/dist/participant/index.d.ts +2 -0
- package/dist/participant/index.js +13 -1
- package/dist/styles.css +34 -0
- package/package.json +2 -2
- package/src/participant/ParticipantBubbleBody.tsx +15 -1
- package/src/participant/ParticipantMessageBubble.test.tsx +35 -0
- package/src/participant/ParticipantMessageBubble.tsx +2 -0
- package/src/participant/participantLabels.ts +4 -0
- package/src/styles.css +6 -1
|
@@ -190,6 +190,8 @@ type ParticipantConversationsLabels = {
|
|
|
190
190
|
readonly edit: string;
|
|
191
191
|
/** Name of the menu button on a failed message. Optional; translated hosts should fill it, otherwise the English default shows. */
|
|
192
192
|
readonly messageOptions?: string;
|
|
193
|
+
/** Toggle that opens the message as received (e.g. the whole email). Optional; the English default shows otherwise. */
|
|
194
|
+
readonly showOriginalText?: string;
|
|
193
195
|
readonly notFound: string;
|
|
194
196
|
readonly loading: string;
|
|
195
197
|
readonly loadError: string;
|
|
@@ -64,6 +64,7 @@ function formatRecordingElapsedLabel(template, elapsed, limit) {
|
|
|
64
64
|
|
|
65
65
|
// src/participant/participantLabels.ts
|
|
66
66
|
var DEFAULT_MESSAGE_OPTIONS_LABEL = "Message options";
|
|
67
|
+
var DEFAULT_SHOW_ORIGINAL_TEXT_LABEL = "Show original";
|
|
67
68
|
var DEFAULT_PARTICIPANT_CONVERSATIONS_LABELS = {
|
|
68
69
|
inboxTitle: "Conversations",
|
|
69
70
|
sectionAwaiting: "Waiting for your reply",
|
|
@@ -101,6 +102,7 @@ var DEFAULT_PARTICIPANT_CONVERSATIONS_LABELS = {
|
|
|
101
102
|
discard: "Discard",
|
|
102
103
|
edit: "Edit",
|
|
103
104
|
messageOptions: DEFAULT_MESSAGE_OPTIONS_LABEL,
|
|
105
|
+
showOriginalText: DEFAULT_SHOW_ORIGINAL_TEXT_LABEL,
|
|
104
106
|
notFound: "Conversation not found",
|
|
105
107
|
loading: "Loading\u2026",
|
|
106
108
|
loadError: "Could not load. Check your connection and try again.",
|
|
@@ -2577,6 +2579,14 @@ function BubbleHeading({ content, labels }) {
|
|
|
2577
2579
|
if (!content.authorName) return null;
|
|
2578
2580
|
return /* @__PURE__ */ jsx27("span", { className: "cv-p-bubble__author", children: content.authorName });
|
|
2579
2581
|
}
|
|
2582
|
+
function OriginalText({ content, labels }) {
|
|
2583
|
+
const original = content.originalText?.trim();
|
|
2584
|
+
if (!original || original === content.text?.trim()) return null;
|
|
2585
|
+
return /* @__PURE__ */ jsxs21("details", { className: "cv-p-bubble__original", children: [
|
|
2586
|
+
/* @__PURE__ */ jsx27("summary", { className: "cv-p-bubble__original-toggle", children: labels.showOriginalText ?? DEFAULT_SHOW_ORIGINAL_TEXT_LABEL }),
|
|
2587
|
+
/* @__PURE__ */ jsx27("div", { className: "cv-p-bubble__original-text", children: original })
|
|
2588
|
+
] });
|
|
2589
|
+
}
|
|
2580
2590
|
function BubbleBody({ content, labels, ownStatus, textId, resolveAttachmentUrl, onRetry, onDiscard, onEdit }) {
|
|
2581
2591
|
return /* @__PURE__ */ jsxs21(Fragment6, { children: [
|
|
2582
2592
|
/* @__PURE__ */ jsx27(BubbleHeading, { content, labels }),
|
|
@@ -2595,7 +2605,8 @@ function BubbleBody({ content, labels, ownStatus, textId, resolveAttachmentUrl,
|
|
|
2595
2605
|
/* @__PURE__ */ jsx27("time", { className: "cv-p-bubble__time", dateTime: content.createdAt, children: formatTimestamp(content.createdAt) }),
|
|
2596
2606
|
ownStatus ? /* @__PURE__ */ jsx27(OwnStatus, { status: ownStatus, labels, onRetry }) : null,
|
|
2597
2607
|
ownStatus === "failed" ? /* @__PURE__ */ jsx27(ParticipantFailedMenu, { labels, onEdit, onDiscard }) : null
|
|
2598
|
-
] })
|
|
2608
|
+
] }),
|
|
2609
|
+
/* @__PURE__ */ jsx27(OriginalText, { content, labels })
|
|
2599
2610
|
] });
|
|
2600
2611
|
}
|
|
2601
2612
|
|
|
@@ -2669,6 +2680,7 @@ function describeItem(item, perspective) {
|
|
|
2669
2680
|
isMine: isOwnMessage(message, perspective),
|
|
2670
2681
|
authorName: message.authorName ?? void 0,
|
|
2671
2682
|
text: message.text ?? void 0,
|
|
2683
|
+
originalText: message.originalText ?? void 0,
|
|
2672
2684
|
createdAt: message.createdAt,
|
|
2673
2685
|
attachments: message.attachments,
|
|
2674
2686
|
pendingFilenames: [],
|
package/dist/styles.css
CHANGED
|
@@ -1978,7 +1978,41 @@
|
|
|
1978
1978
|
.cv-p-bubble > .cv-p-attachment {
|
|
1979
1979
|
flex: 1 0 100%;
|
|
1980
1980
|
}
|
|
1981
|
+
.cv-p-bubble__original {
|
|
1982
|
+
flex: 1 0 100%;
|
|
1983
|
+
margin-top: 0.2rem;
|
|
1984
|
+
}
|
|
1985
|
+
.cv-p-bubble__original-toggle {
|
|
1986
|
+
display: inline-block;
|
|
1987
|
+
font-size: 0.7rem;
|
|
1988
|
+
color: var(--cv-p-i-text-muted);
|
|
1989
|
+
cursor: pointer;
|
|
1990
|
+
list-style: none;
|
|
1991
|
+
text-decoration: underline;
|
|
1992
|
+
text-decoration-style: dotted;
|
|
1993
|
+
text-underline-offset: 2px;
|
|
1994
|
+
}
|
|
1995
|
+
.cv-p-bubble__original-toggle::-webkit-details-marker {
|
|
1996
|
+
display: none;
|
|
1997
|
+
}
|
|
1998
|
+
.cv-p-bubble__original-toggle:focus-visible {
|
|
1999
|
+
outline: 2px solid var(--cv-p-i-accent);
|
|
2000
|
+
outline-offset: 2px;
|
|
2001
|
+
border-radius: 2px;
|
|
2002
|
+
}
|
|
2003
|
+
.cv-p-bubble__original-text {
|
|
2004
|
+
margin-top: 0.3rem;
|
|
2005
|
+
padding-inline-start: 0.5rem;
|
|
2006
|
+
border-inline-start: 2px solid var(--cv-p-i-border);
|
|
2007
|
+
max-height: 16rem;
|
|
2008
|
+
overflow: auto;
|
|
2009
|
+
white-space: pre-wrap;
|
|
2010
|
+
overflow-wrap: anywhere;
|
|
2011
|
+
font-size: 0.8rem;
|
|
2012
|
+
color: var(--cv-p-i-text-muted);
|
|
2013
|
+
}
|
|
1981
2014
|
.cv-p-bubble__meta {
|
|
2015
|
+
order: 1;
|
|
1982
2016
|
display: flex;
|
|
1983
2017
|
flex: 0 0 auto;
|
|
1984
2018
|
justify-content: flex-end;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adatechnology/conversations-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "WhatsApp conversation UI components — parametrizável por endpoint, tema e feature flags",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"clsx": "^2.1.1",
|
|
36
36
|
"lucide-react": "^1.21.0",
|
|
37
37
|
"tailwind-merge": "^3.6.0",
|
|
38
|
-
"@adatechnology/conversation-contracts": "0.
|
|
38
|
+
"@adatechnology/conversation-contracts": "0.7.0",
|
|
39
39
|
"@adatechnology/meta-whatsapp-contracts": "0.6.0"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
@@ -4,7 +4,7 @@ import { OwnStatus } from './ParticipantBubbleStatus'
|
|
|
4
4
|
import { ParticipantFailedMenu } from './ParticipantFailedMenu'
|
|
5
5
|
import type { BubbleContent } from './ParticipantMessageBubble'
|
|
6
6
|
import { ParticipantMessageText } from './ParticipantMessageText'
|
|
7
|
-
import type
|
|
7
|
+
import { DEFAULT_SHOW_ORIGINAL_TEXT_LABEL, type ParticipantConversationsLabels } from './participantLabels'
|
|
8
8
|
import type { ParticipantOwnMessageStatus } from './participantMessages'
|
|
9
9
|
|
|
10
10
|
type BubbleBodyProps = {
|
|
@@ -24,6 +24,18 @@ function BubbleHeading({ content, labels }: Pick<BubbleBodyProps, 'content' | 'l
|
|
|
24
24
|
return <span className="cv-p-bubble__author">{content.authorName}</span>
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/** Collapsed by default: the trimmed text is what the reader needs, the original is there on demand. */
|
|
28
|
+
function OriginalText({ content, labels }: Pick<BubbleBodyProps, 'content' | 'labels'>) {
|
|
29
|
+
const original = content.originalText?.trim()
|
|
30
|
+
if (!original || original === content.text?.trim()) return null
|
|
31
|
+
return (
|
|
32
|
+
<details className="cv-p-bubble__original">
|
|
33
|
+
<summary className="cv-p-bubble__original-toggle">{labels.showOriginalText ?? DEFAULT_SHOW_ORIGINAL_TEXT_LABEL}</summary>
|
|
34
|
+
<div className="cv-p-bubble__original-text">{original}</div>
|
|
35
|
+
</details>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
27
39
|
export function BubbleBody({ content, labels, ownStatus, textId, resolveAttachmentUrl, onRetry, onDiscard, onEdit }: BubbleBodyProps) {
|
|
28
40
|
return (
|
|
29
41
|
<>
|
|
@@ -49,6 +61,8 @@ export function BubbleBody({ content, labels, ownStatus, textId, resolveAttachme
|
|
|
49
61
|
{ownStatus ? <OwnStatus status={ownStatus} labels={labels} onRetry={onRetry} /> : null}
|
|
50
62
|
{ownStatus === 'failed' ? <ParticipantFailedMenu labels={labels} onEdit={onEdit} onDiscard={onDiscard} /> : null}
|
|
51
63
|
</span>
|
|
64
|
+
{/* Last child so the ids React derives for the siblings above stay as they were; CSS keeps the meta after it. */}
|
|
65
|
+
<OriginalText content={content} labels={labels} />
|
|
52
66
|
</>
|
|
53
67
|
)
|
|
54
68
|
}
|
|
@@ -70,6 +70,41 @@ describe('ParticipantMessageBubble', () => {
|
|
|
70
70
|
expect(mine).not.toContain('Marina')
|
|
71
71
|
})
|
|
72
72
|
|
|
73
|
+
it('keeps the original text collapsed behind a discreet toggle inside the bubble', () => {
|
|
74
|
+
const original = 'Pode descarregar.\n\nEm ter., 20 de out. de 2026, Equipe escreveu:\n> Autorizam?'
|
|
75
|
+
const markup = renderServer(buildMessage({ id: 'a', text: 'Pode descarregar.', originalText: original }))
|
|
76
|
+
|
|
77
|
+
expect(markup).toContain('<details class="cv-p-bubble__original">')
|
|
78
|
+
expect(markup).not.toContain('<details class="cv-p-bubble__original" open')
|
|
79
|
+
expect(markup).toContain('<summary class="cv-p-bubble__original-toggle">Show original</summary>')
|
|
80
|
+
expect(markup).toContain('Em ter., 20 de out. de 2026, Equipe escreveu:')
|
|
81
|
+
expect(findUtilityClassTokens(markup)).toEqual([])
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('uses the host label for the original-text toggle', () => {
|
|
85
|
+
const item: ParticipantTimelineItem = {
|
|
86
|
+
kind: 'server',
|
|
87
|
+
message: buildMessage({ id: 'a', text: 'Ok', originalText: 'Ok\n> antes' }),
|
|
88
|
+
}
|
|
89
|
+
const markup = renderToStaticMarkup(
|
|
90
|
+
<ParticipantMessageBubble
|
|
91
|
+
item={item}
|
|
92
|
+
labels={{ ...LABELS, showOriginalText: 'ver histórico' }}
|
|
93
|
+
confirmsRead
|
|
94
|
+
resolveAttachmentUrl={resolveAttachmentUrl}
|
|
95
|
+
/>,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
expect(markup).toContain('>ver histórico</summary>')
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('shows no toggle when the original is absent, blank or the same as the text', () => {
|
|
102
|
+
expect(renderServer(buildMessage({ id: 'a', text: 'Ok' }))).not.toContain('<details')
|
|
103
|
+
expect(renderServer(buildMessage({ id: 'b', text: 'Ok', originalText: null }))).not.toContain('<details')
|
|
104
|
+
expect(renderServer(buildMessage({ id: 'c', text: 'Ok', originalText: ' ' }))).not.toContain('<details')
|
|
105
|
+
expect(renderServer(buildMessage({ id: 'd', text: 'Ok', originalText: ' Ok\n' }))).not.toContain('<details')
|
|
106
|
+
})
|
|
107
|
+
|
|
73
108
|
it('renders a time element with the ISO dateTime', () => {
|
|
74
109
|
expect(renderServer()).toMatch(/<time[^>]*dateTime="2026-10-01T10:00:00.000Z"/)
|
|
75
110
|
})
|
|
@@ -37,6 +37,7 @@ export type BubbleContent = {
|
|
|
37
37
|
readonly isMine: boolean
|
|
38
38
|
readonly authorName?: string
|
|
39
39
|
readonly text?: string
|
|
40
|
+
readonly originalText?: string
|
|
40
41
|
readonly createdAt: string
|
|
41
42
|
readonly attachments: readonly ParticipantAttachment[]
|
|
42
43
|
readonly pendingFilenames: readonly string[]
|
|
@@ -51,6 +52,7 @@ function describeItem(item: ParticipantTimelineItem, perspective?: ParticipantPe
|
|
|
51
52
|
isMine: isOwnMessage(message, perspective),
|
|
52
53
|
authorName: message.authorName ?? undefined,
|
|
53
54
|
text: message.text ?? undefined,
|
|
55
|
+
originalText: message.originalText ?? undefined,
|
|
54
56
|
createdAt: message.createdAt,
|
|
55
57
|
attachments: message.attachments,
|
|
56
58
|
pendingFilenames: [],
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { DEFAULT_PARTICIPANT_AUDIO_LABELS, type ParticipantAudioLabels } from './participantAudioLabels'
|
|
2
2
|
|
|
3
3
|
export const DEFAULT_MESSAGE_OPTIONS_LABEL = 'Message options'
|
|
4
|
+
export const DEFAULT_SHOW_ORIGINAL_TEXT_LABEL = 'Show original'
|
|
4
5
|
|
|
5
6
|
export type ParticipantConversationsLabels = {
|
|
6
7
|
readonly inboxTitle: string
|
|
@@ -43,6 +44,8 @@ export type ParticipantConversationsLabels = {
|
|
|
43
44
|
readonly edit: string
|
|
44
45
|
/** Name of the menu button on a failed message. Optional; translated hosts should fill it, otherwise the English default shows. */
|
|
45
46
|
readonly messageOptions?: string
|
|
47
|
+
/** Toggle that opens the message as received (e.g. the whole email). Optional; the English default shows otherwise. */
|
|
48
|
+
readonly showOriginalText?: string
|
|
46
49
|
readonly notFound: string
|
|
47
50
|
readonly loading: string
|
|
48
51
|
readonly loadError: string
|
|
@@ -106,6 +109,7 @@ export const DEFAULT_PARTICIPANT_CONVERSATIONS_LABELS: ParticipantConversationsL
|
|
|
106
109
|
discard: 'Discard',
|
|
107
110
|
edit: 'Edit',
|
|
108
111
|
messageOptions: DEFAULT_MESSAGE_OPTIONS_LABEL,
|
|
112
|
+
showOriginalText: DEFAULT_SHOW_ORIGINAL_TEXT_LABEL,
|
|
109
113
|
notFound: 'Conversation not found',
|
|
110
114
|
loading: 'Loading…',
|
|
111
115
|
loadError: 'Could not load. Check your connection and try again.',
|
package/src/styles.css
CHANGED
|
@@ -1302,7 +1302,12 @@
|
|
|
1302
1302
|
.cv-p-bubble__author { display: block; flex: 1 0 100%; margin-bottom: 0.15rem; font: 0.65rem 'SFMono-Regular', Consolas, monospace; text-transform: uppercase; color: var(--cv-p-i-accent); }
|
|
1303
1303
|
.cv-p-bubble > .cv-p-text { flex: 0 1 auto; min-width: 0; margin-inline-end: auto; }
|
|
1304
1304
|
.cv-p-bubble > .cv-p-attachment { flex: 1 0 100%; }
|
|
1305
|
-
.cv-p-
|
|
1305
|
+
.cv-p-bubble__original { flex: 1 0 100%; margin-top: 0.2rem; }
|
|
1306
|
+
.cv-p-bubble__original-toggle { display: inline-block; font-size: 0.7rem; color: var(--cv-p-i-text-muted); cursor: pointer; list-style: none; text-decoration: underline; text-decoration-style: dotted; text-underline-offset: 2px; }
|
|
1307
|
+
.cv-p-bubble__original-toggle::-webkit-details-marker { display: none; }
|
|
1308
|
+
.cv-p-bubble__original-toggle:focus-visible { outline: 2px solid var(--cv-p-i-accent); outline-offset: 2px; border-radius: 2px; }
|
|
1309
|
+
.cv-p-bubble__original-text { margin-top: 0.3rem; padding-inline-start: 0.5rem; border-inline-start: 2px solid var(--cv-p-i-border); max-height: 16rem; overflow: auto; white-space: pre-wrap; overflow-wrap: anywhere; font-size: 0.8rem; color: var(--cv-p-i-text-muted); }
|
|
1310
|
+
.cv-p-bubble__meta { order: 1; display: flex; flex: 0 0 auto; justify-content: flex-end; align-items: center; gap: 0.35rem; margin-inline-start: auto; margin-top: 0.25rem; font: 0.65rem 'SFMono-Regular', Consolas, monospace; color: var(--cv-p-i-text-muted); }
|
|
1306
1311
|
.cv-p-bubble__status { display: inline-flex; align-items: center; gap: 0.25rem; }
|
|
1307
1312
|
.cv-p-bubble .cv-status-ticks--queued, .cv-p-bubble .cv-status-ticks--sent, .cv-p-bubble .cv-status-ticks--delivered { color: var(--cv-p-i-tick); }
|
|
1308
1313
|
.cv-p-bubble .cv-status-ticks--read { color: var(--cv-p-i-tick-read); }
|