@adatechnology/conversations-ui 0.7.0 → 0.8.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 +96 -1
- package/dist/{ConversationSimulatorPanel-CrfFjiWh.d.ts → ConversationSimulatorPanel-q-wVgBCN.d.ts} +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/participant/index.d.ts +45 -3
- package/dist/participant/index.js +287 -112
- package/dist/preview/index.d.ts +3 -3
- package/dist/{quickReply.types-B4gmZsSr.d.ts → quickReply.types-DzWH3NYV.d.ts} +1 -1
- package/dist/styles.css +74 -29
- package/package.json +1 -1
- package/src/participant/ConversationThread.test.tsx +165 -0
- package/src/participant/ConversationThread.tsx +117 -0
- package/src/participant/ParticipantMessageBubble.tsx +7 -3
- package/src/participant/ParticipantThread.tsx +9 -0
- package/src/participant/ParticipantThreadHeader.tsx +10 -0
- package/src/participant/ParticipantThreadScreen.tsx +6 -46
- package/src/participant/ParticipantTimeline.tsx +6 -3
- package/src/participant/conversationThreadApi.test.ts +27 -0
- package/src/participant/conversationThreadApi.ts +24 -0
- package/src/participant/conversationThreadCss.test.ts +116 -0
- package/src/participant/conversationThreadSummary.ts +34 -0
- package/src/participant/index.ts +3 -0
- package/src/participant/participantContrast.test-helper.ts +116 -0
- package/src/participant/participantGolden.test.tsx +46 -0
- package/src/participant/participantGoldenScenarios.test-helper.tsx +57 -0
- package/src/participant/participantMessages.ts +4 -3
- package/src/participant/participantNewMessages.test.ts +21 -0
- package/src/participant/participantNewMessages.ts +8 -3
- package/src/participant/participantPerspective.test.ts +67 -0
- package/src/participant/participantPerspective.ts +50 -0
- package/src/participant/participantPerspectiveRender.test.tsx +94 -0
- package/src/participant/participantThemeContrast.test.ts +98 -0
- package/src/participant/test-fixtures/conversations-list.golden.html +1 -0
- package/src/participant/test-fixtures/conversations-selected.golden.html +1 -0
- package/src/participant/test-fixtures/thread-avatars-initials.golden.html +1 -0
- package/src/participant/test-fixtures/thread-closed.golden.html +1 -0
- package/src/participant/test-fixtures/thread-default.golden.html +1 -0
- package/src/participant/test-fixtures/thread-error.golden.html +1 -0
- package/src/participant/test-fixtures/thread-loading.golden.html +1 -0
- package/src/participant/test-fixtures/thread-plain.golden.html +1 -0
- package/src/participant/test-fixtures/thread-tail-off.golden.html +1 -0
- package/src/participant/useParticipantConversationView.ts +3 -0
- package/src/participant/useParticipantThread.ts +13 -2
- package/src/participant/useParticipantThreadController.ts +112 -0
- package/src/participant/useParticipantThreadScroll.ts +3 -1
- package/src/participant/useParticipantUnread.test.tsx +40 -0
- package/src/participant/useParticipantUnread.ts +38 -0
- package/src/styles.css +24 -12
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test'
|
|
2
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
3
|
+
|
|
4
|
+
import type { ParticipantMessage } from '@adatechnology/conversation-contracts'
|
|
5
|
+
|
|
6
|
+
import { buildMessage } from './participantFixtures.test-helper'
|
|
7
|
+
import type { ParticipantPerspective } from './participantPerspective'
|
|
8
|
+
import { useParticipantUnread } from './useParticipantUnread'
|
|
9
|
+
|
|
10
|
+
const MESSAGES: readonly ParticipantMessage[] = [
|
|
11
|
+
buildMessage({ id: 'a', direction: 'inbound' }),
|
|
12
|
+
buildMessage({ id: 'b', direction: 'inbound' }),
|
|
13
|
+
buildMessage({ id: 'c', direction: 'outbound' }),
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
type ProbeProps = { readonly perspective?: ParticipantPerspective; readonly reported: number }
|
|
17
|
+
|
|
18
|
+
function Probe({ perspective, reported }: ProbeProps) {
|
|
19
|
+
const { unreadCount } = useParticipantUnread({ messages: MESSAGES, perspective, reportedUnreadCount: reported })
|
|
20
|
+
return <output>{unreadCount}</output>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function unreadOf(props: ProbeProps): string {
|
|
24
|
+
return renderToStaticMarkup(<Probe {...props} />)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('useParticipantUnread', () => {
|
|
28
|
+
it('keeps the count the inbox reported when no perspective is given', () => {
|
|
29
|
+
expect(unreadOf({ reported: 3 })).toBe('<output>3</output>')
|
|
30
|
+
expect(unreadOf({ reported: 0 })).toBe('<output>0</output>')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('derives from the messages for the operator: only the inbound ones, whatever the inbox said', () => {
|
|
34
|
+
expect(unreadOf({ perspective: 'operator', reported: 9 })).toBe('<output>2</output>')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('derives from the messages for the participant: only the outbound ones', () => {
|
|
38
|
+
expect(unreadOf({ perspective: 'participant', reported: 9 })).toBe('<output>1</output>')
|
|
39
|
+
})
|
|
40
|
+
})
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { useCallback, useRef, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
import type { ParticipantMessage, ParticipantSubjectRef } from '@adatechnology/conversation-contracts'
|
|
4
|
+
|
|
5
|
+
import { countOtherSideMessages, resolveViewerUnread, type ParticipantPerspective } from './participantPerspective'
|
|
6
|
+
|
|
7
|
+
export type UseParticipantUnreadParams = {
|
|
8
|
+
readonly messages: readonly ParticipantMessage[]
|
|
9
|
+
/** Absent keeps the count the inbox reported, which is how the list-driven screen works. */
|
|
10
|
+
readonly perspective?: ParticipantPerspective
|
|
11
|
+
readonly reportedUnreadCount: number
|
|
12
|
+
readonly onMarkedRead?: (subject: ParticipantSubjectRef) => void
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type UseParticipantUnreadResult = {
|
|
16
|
+
readonly unreadCount: number
|
|
17
|
+
readonly handleMarkedRead: (subject: ParticipantSubjectRef) => void
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** A single conversation has no inbox, so what the viewer has not read is derived from the messages themselves. */
|
|
21
|
+
export function useParticipantUnread(params: UseParticipantUnreadParams): UseParticipantUnreadResult {
|
|
22
|
+
const { messages, perspective, reportedUnreadCount, onMarkedRead } = params
|
|
23
|
+
const [acknowledgedCount, setAcknowledgedCount] = useState(0)
|
|
24
|
+
const messagesRef = useRef(messages)
|
|
25
|
+
messagesRef.current = messages
|
|
26
|
+
|
|
27
|
+
const handleMarkedRead = useCallback(
|
|
28
|
+
(subject: ParticipantSubjectRef): void => {
|
|
29
|
+
if (perspective) setAcknowledgedCount(countOtherSideMessages(messagesRef.current, perspective))
|
|
30
|
+
onMarkedRead?.(subject)
|
|
31
|
+
},
|
|
32
|
+
[perspective, onMarkedRead],
|
|
33
|
+
)
|
|
34
|
+
const unreadCount = perspective
|
|
35
|
+
? resolveViewerUnread({ messages, perspective, acknowledgedCount })
|
|
36
|
+
: reportedUnreadCount
|
|
37
|
+
return { unreadCount, handleMarkedRead }
|
|
38
|
+
}
|
package/src/styles.css
CHANGED
|
@@ -1139,7 +1139,7 @@
|
|
|
1139
1139
|
|
|
1140
1140
|
/* Participant view (.cv-p-*). Public theming API: override --cv-p-surface, --cv-p-surface-raised,
|
|
1141
1141
|
--cv-p-text, --cv-p-text-muted, --cv-p-border, --cv-p-accent, --cv-p-accent-contrast,
|
|
1142
|
-
--cv-p-highlight, --cv-p-danger, --cv-p-radius, --cv-p-wallpaper, --cv-p-scrollbar-thumb, --cv-p-scrollbar-thumb-hover, --cv-p-channel-app, --cv-p-channel-whatsapp on .cv-p or any
|
|
1142
|
+
--cv-p-highlight, --cv-p-danger, --cv-p-radius, --cv-p-wallpaper, --cv-p-scrollbar-thumb, --cv-p-scrollbar-thumb-hover, --cv-p-tick, --cv-p-tick-read, --cv-p-channel-app, --cv-p-channel-whatsapp on .cv-p or any
|
|
1143
1143
|
ancestor. Channel colors must keep 3:1 against surface, surface-raised and highlight. */
|
|
1144
1144
|
.cv-p {
|
|
1145
1145
|
--cv-p-i-surface: var(--cv-p-surface, #ffffff);
|
|
@@ -1149,12 +1149,14 @@
|
|
|
1149
1149
|
--cv-p-i-border: var(--cv-p-border, #d5d9df);
|
|
1150
1150
|
--cv-p-i-accent: var(--cv-p-accent, #a85a1c);
|
|
1151
1151
|
--cv-p-i-accent-contrast: var(--cv-p-accent-contrast, #ffffff);
|
|
1152
|
-
--cv-p-i-highlight: var(--cv-p-highlight, #
|
|
1152
|
+
--cv-p-i-highlight: var(--cv-p-highlight, #fcf3ea);
|
|
1153
1153
|
--cv-p-i-danger: var(--cv-p-danger, #c62828);
|
|
1154
1154
|
--cv-p-i-radius: var(--cv-p-radius, 0);
|
|
1155
1155
|
--cv-p-i-touch: 2.75rem;
|
|
1156
1156
|
--cv-p-i-scrollbar-thumb: var(--cv-p-scrollbar-thumb, var(--cv-p-i-border));
|
|
1157
1157
|
--cv-p-i-scrollbar-thumb-hover: var(--cv-p-scrollbar-thumb-hover, var(--cv-p-i-accent));
|
|
1158
|
+
--cv-p-i-tick: var(--cv-p-tick, var(--cv-p-i-text-muted));
|
|
1159
|
+
--cv-p-i-tick-read: var(--cv-p-tick-read, #0369a1);
|
|
1158
1160
|
--cv-p-i-wallpaper-ink: var(--cv-p-i-border);
|
|
1159
1161
|
--cv-p-i-wallpaper: radial-gradient(circle at 2px 2px, var(--cv-p-i-wallpaper-ink) 1px, transparent 1.5px), radial-gradient(circle at 14px 14px, var(--cv-p-i-wallpaper-ink) 1px, transparent 1.5px);
|
|
1160
1162
|
display: flex;
|
|
@@ -1177,6 +1179,7 @@
|
|
|
1177
1179
|
--cv-p-i-accent-contrast: var(--cv-p-accent-contrast, #10222c);
|
|
1178
1180
|
--cv-p-i-highlight: var(--cv-p-highlight, rgb(213 138 71 / 10%));
|
|
1179
1181
|
--cv-p-i-danger: var(--cv-p-danger, #ff6b63);
|
|
1182
|
+
--cv-p-i-tick-read: var(--cv-p-tick-read, #5cc8ff);
|
|
1180
1183
|
}
|
|
1181
1184
|
.cv-p *, .cv-p *::before, .cv-p *::after { box-sizing: border-box; }
|
|
1182
1185
|
.cv-p :focus-visible { outline: 2px solid var(--cv-p-i-accent); outline-offset: 2px; }
|
|
@@ -1211,6 +1214,7 @@
|
|
|
1211
1214
|
.cv-p-filters { --cv-p-i-fade: 1rem; display: flex; gap: 0.5rem; padding: 0.625rem 1rem; overflow-x: auto; border-bottom: 1px solid var(--cv-p-i-border); }
|
|
1212
1215
|
.cv-p-error { display: flex; flex-direction: column; align-items: center; gap: 0.5rem; padding: 1.5rem 1rem; text-align: center; color: var(--cv-p-i-danger, #b91c1c); }
|
|
1213
1216
|
.cv-p-error__message { margin: 0; }
|
|
1217
|
+
.cv-p-thread__scroll > .cv-p-error { background: var(--cv-p-i-surface); }
|
|
1214
1218
|
.cv-p-loading { display: flex; flex-direction: column; gap: 0.5rem; padding: 1rem; }
|
|
1215
1219
|
.cv-p-skeleton { display: block; height: 3rem; border-radius: var(--cv-p-i-radius); background: var(--cv-p-i-surface-raised); }
|
|
1216
1220
|
.cv-p-inbox__more { display: block; margin: 0.75rem auto; padding: 0 1rem; }
|
|
@@ -1258,6 +1262,10 @@
|
|
|
1258
1262
|
.cv-p-thread__eyebrow { margin: 0; font: 0.64rem 'SFMono-Regular', Consolas, monospace; letter-spacing: 0.08em; text-transform: uppercase; color: var(--cv-p-i-text-muted); }
|
|
1259
1263
|
.cv-p-thread__title { margin: 0; font-size: 1rem; font-weight: 600; line-height: 1.3; min-width: 0; overflow-wrap: anywhere; }
|
|
1260
1264
|
.cv-p-thread__title-text { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 1; line-clamp: 1; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
|
|
1265
|
+
.cv-p-thread__counterpart { margin: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 0.78rem; line-height: 1.3; color: var(--cv-p-i-text-muted); }
|
|
1266
|
+
.cv-p-thread__actions { flex: 0 0 auto; display: flex; align-items: center; gap: 0.5rem; margin-inline-start: auto; max-width: 100%; }
|
|
1267
|
+
.cv-p-thread__header:has(.cv-p-thread__actions) { flex-wrap: wrap; }
|
|
1268
|
+
.cv-p-thread__header:has(.cv-p-thread__actions) .cv-p-thread__heading { min-width: 6rem; }
|
|
1261
1269
|
.cv-p-thread__meta { display: flex; flex-wrap: wrap; align-items: center; gap: 0.125rem 0.5rem; min-width: 0; padding-top: 0.5rem; }
|
|
1262
1270
|
.cv-p-thread__title-button { display: flex; align-items: center; max-width: 100%; min-height: var(--cv-p-i-touch); padding: 0; background: none; border: 0; color: inherit; font: inherit; font-weight: 600; text-align: start; text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 0.2em; cursor: pointer; }
|
|
1263
1271
|
.cv-p-thread__subject-card { margin: 0.75rem 1rem 0; padding: 0.625rem 0.75rem; border: 1px solid var(--cv-p-i-border); background: var(--cv-p-i-surface-raised); border-radius: var(--cv-p-i-radius); font-size: 0.82rem; }
|
|
@@ -1265,7 +1273,7 @@
|
|
|
1265
1273
|
.cv-p-thread__scroll { flex: 1; min-height: 0; overflow-y: auto; overflow-anchor: none; touch-action: pan-y pinch-zoom; padding: 0.5rem 0.75rem 1rem; background-image: var(--cv-p-wallpaper, var(--cv-p-i-wallpaper)); background-size: var(--cv-p-wallpaper-size, 24px 24px); }
|
|
1266
1274
|
.cv-p-thread__older { display: block; margin: 0 auto 0.5rem; min-height: var(--cv-p-i-touch); padding: 0 1rem; background: transparent; border: 1px solid var(--cv-p-i-border); border-radius: var(--cv-p-i-radius); }
|
|
1267
1275
|
.cv-p-thread__messages { display: flex; flex-direction: column; gap: 0.5rem; }
|
|
1268
|
-
.cv-p-thread__live { padding: 0 1rem; font-size: 0.75rem; color: var(--cv-p-i-accent); }
|
|
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); }
|
|
1269
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; }
|
|
1270
1278
|
.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); }
|
|
1271
1279
|
.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); }
|
|
@@ -1285,14 +1293,6 @@
|
|
|
1285
1293
|
.cv-p-bubble-row > .cv-p-bubble:not(.cv-p-bubble--no-tail) { margin-left: 0.25rem; }
|
|
1286
1294
|
.cv-p-bubble--no-tail { border-radius: var(--cv-p-i-radius); }
|
|
1287
1295
|
.cv-p-bubble--no-tail::before, .cv-p-bubble--no-tail::after { content: none; }
|
|
1288
|
-
@media (forced-colors: active) {
|
|
1289
|
-
.cv-p-bubble::before, .cv-p-bubble::after { content: none; }
|
|
1290
|
-
.cv-p-bubble, .cv-p-bubble--mine { border-radius: var(--cv-p-i-radius); }
|
|
1291
|
-
.cv-p-thread__scroll { background-image: none; }
|
|
1292
|
-
.cv-p-thread__scroll, .cv-p-inbox, .cv-p-composer__input { scrollbar-width: auto; scrollbar-color: auto; }
|
|
1293
|
-
.cv-p-filters, .cv-p-quick { mask-image: none; -webkit-mask-image: none; }
|
|
1294
|
-
.cv-p-thread__latest-badge { border: 1px solid ButtonText; }
|
|
1295
|
-
}
|
|
1296
1296
|
@media print {
|
|
1297
1297
|
.cv-p-bubble::before, .cv-p-bubble::after { content: none; }
|
|
1298
1298
|
.cv-p-bubble, .cv-p-bubble--mine { border-radius: var(--cv-p-i-radius); }
|
|
@@ -1303,6 +1303,9 @@
|
|
|
1303
1303
|
.cv-p-bubble > .cv-p-attachment, .cv-p-bubble > .cv-p-bubble__actions { flex: 1 0 100%; }
|
|
1304
1304
|
.cv-p-bubble__meta { 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); }
|
|
1305
1305
|
.cv-p-bubble__status { display: inline-flex; align-items: center; gap: 0.25rem; }
|
|
1306
|
+
.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); }
|
|
1307
|
+
.cv-p-bubble .cv-status-ticks--read { color: var(--cv-p-i-tick-read); }
|
|
1308
|
+
.cv-p-bubble .cv-status-ticks--failed { color: var(--cv-p-i-danger); }
|
|
1306
1309
|
.cv-p-bubble__retry { min-height: var(--cv-p-i-touch); display: inline-flex; align-items: center; gap: 0.25rem; padding: 0 0.5rem; background: transparent; border: 1px solid var(--cv-p-i-danger); color: var(--cv-p-i-danger); border-radius: var(--cv-p-i-radius); font-size: 0.7rem; }
|
|
1307
1310
|
.cv-p-bubble__actions { display: inline-flex; gap: 0.5rem; margin-top: 0.25rem; }
|
|
1308
1311
|
.cv-p-bubble__action { min-height: var(--cv-p-i-touch); display: inline-flex; align-items: center; padding: 0 0.75rem; background: transparent; border: 1px solid var(--cv-p-i-border); color: inherit; border-radius: var(--cv-p-i-radius); font-size: 0.7rem; }
|
|
@@ -1405,7 +1408,16 @@
|
|
|
1405
1408
|
.cv-p-text__link-copy::after { inset-inline: 0 auto; min-width: var(--cv-p-i-touch); }
|
|
1406
1409
|
.cv-p-copyable { padding: 0 0.15rem; font-family: 'SFMono-Regular', Consolas, monospace; font-size: 0.92em; background: color-mix(in srgb, var(--cv-p-i-accent) 14%, transparent); border-bottom: 1px dotted var(--cv-p-i-accent); border-radius: var(--cv-p-i-radius); }
|
|
1407
1410
|
.cv-p-copyable--email { font-family: inherit; }
|
|
1408
|
-
.cv-p-copyable__done { margin-inline-start: 0.35rem; font-size: 0.85em; color: var(--cv-p-i-
|
|
1411
|
+
.cv-p-copyable__done { margin-inline-start: 0.35rem; font-size: 0.85em; color: var(--cv-p-i-text); }
|
|
1412
|
+
@media (forced-colors: active) {
|
|
1413
|
+
.cv-p-bubble::before, .cv-p-bubble::after { content: none; }
|
|
1414
|
+
.cv-p-bubble, .cv-p-bubble--mine { border-radius: var(--cv-p-i-radius); }
|
|
1415
|
+
.cv-p-thread__scroll { background-image: none; }
|
|
1416
|
+
.cv-p-thread__scroll, .cv-p-inbox, .cv-p-composer__input { scrollbar-width: auto; scrollbar-color: auto; }
|
|
1417
|
+
.cv-p-filters, .cv-p-quick { mask-image: none; -webkit-mask-image: none; }
|
|
1418
|
+
.cv-p-thread__latest-badge { border: 1px solid ButtonText; }
|
|
1419
|
+
.cv-p-bubble .cv-status-ticks, .cv-p-copyable__done { color: CanvasText; }
|
|
1420
|
+
}
|
|
1409
1421
|
@media (forced-colors: active) {
|
|
1410
1422
|
.cv-p-text__code, .cv-p-text__pre, .cv-p-copyable { border: 1px solid; background: none; }
|
|
1411
1423
|
.cv-p-text__link, .cv-p-text__link-copy { color: LinkText; }
|