@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,50 @@
|
|
|
1
|
+
import type { MessageDirection, ParticipantMessage } from '@adatechnology/conversation-contracts'
|
|
2
|
+
|
|
3
|
+
/** Who is looking at the conversation. `participant` is the outside party; `operator` is the company. */
|
|
4
|
+
export type ParticipantPerspective = 'participant' | 'operator'
|
|
5
|
+
|
|
6
|
+
export const DEFAULT_PARTICIPANT_PERSPECTIVE: ParticipantPerspective = 'participant'
|
|
7
|
+
|
|
8
|
+
/** `direction` is from the company side: the participant writes `inbound`, the operator writes `outbound`. */
|
|
9
|
+
const OWN_DIRECTION: Record<ParticipantPerspective, MessageDirection> = {
|
|
10
|
+
participant: 'inbound',
|
|
11
|
+
operator: 'outbound',
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function isMessageMine(
|
|
15
|
+
message: Pick<ParticipantMessage, 'direction'>,
|
|
16
|
+
perspective: ParticipantPerspective = DEFAULT_PARTICIPANT_PERSPECTIVE,
|
|
17
|
+
): boolean {
|
|
18
|
+
return message.direction === OWN_DIRECTION[perspective]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type ReadState = Pick<ParticipantMessage, 'direction' | 'status' | 'readAt'>
|
|
22
|
+
|
|
23
|
+
function isUnreadByViewer(message: ReadState, perspective: ParticipantPerspective): boolean {
|
|
24
|
+
if (isMessageMine(message, perspective)) return false
|
|
25
|
+
return !message.readAt && message.status !== 'read'
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function countOtherSideMessages(
|
|
29
|
+
messages: readonly Pick<ParticipantMessage, 'direction'>[],
|
|
30
|
+
perspective: ParticipantPerspective,
|
|
31
|
+
): number {
|
|
32
|
+
return messages.filter((message) => !isMessageMine(message, perspective)).length
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function countUnreadByViewer(messages: readonly ReadState[], perspective: ParticipantPerspective): number {
|
|
36
|
+
return messages.filter((message) => isUnreadByViewer(message, perspective)).length
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type ResolveViewerUnreadParams = {
|
|
40
|
+
readonly messages: readonly ReadState[]
|
|
41
|
+
readonly perspective: ParticipantPerspective
|
|
42
|
+
/** How many other-side messages the viewer already acknowledged through markRead. */
|
|
43
|
+
readonly acknowledgedCount: number
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Unread other-side messages, or zero while nothing arrived since the last acknowledgement. */
|
|
47
|
+
export function resolveViewerUnread({ messages, perspective, acknowledgedCount }: ResolveViewerUnreadParams): number {
|
|
48
|
+
if (countOtherSideMessages(messages, perspective) <= acknowledgedCount) return 0
|
|
49
|
+
return countUnreadByViewer(messages, perspective)
|
|
50
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test'
|
|
2
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
3
|
+
|
|
4
|
+
import { buildConversation, buildMessage } from './participantFixtures.test-helper'
|
|
5
|
+
import { DEFAULT_PARTICIPANT_CONVERSATIONS_LABELS } from './participantLabels'
|
|
6
|
+
import type { ParticipantTimelineItem } from './participantMessages'
|
|
7
|
+
import { ParticipantThread, type ParticipantThreadProps } from './ParticipantThread'
|
|
8
|
+
|
|
9
|
+
const ITEMS: readonly ParticipantTimelineItem[] = [
|
|
10
|
+
{
|
|
11
|
+
kind: 'server',
|
|
12
|
+
message: buildMessage({
|
|
13
|
+
id: 'a',
|
|
14
|
+
direction: 'inbound',
|
|
15
|
+
authorName: 'Ana Souza',
|
|
16
|
+
createdAt: '2026-10-01T10:00:00.000Z',
|
|
17
|
+
}),
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
kind: 'server',
|
|
21
|
+
message: buildMessage({ id: 'b', direction: 'outbound', status: 'read', createdAt: '2026-10-01T10:05:00.000Z' }),
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
kind: 'pending',
|
|
25
|
+
origin: 'local',
|
|
26
|
+
displayState: 'queued',
|
|
27
|
+
pending: {
|
|
28
|
+
clientMessageId: 'c',
|
|
29
|
+
subject: { subjectType: 'invoice', subjectId: '1' },
|
|
30
|
+
text: 'Waiting',
|
|
31
|
+
createdAt: '2026-10-01T10:06:00.000Z',
|
|
32
|
+
state: 'queued',
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
function render(overrides: Partial<ParticipantThreadProps> = {}): string {
|
|
38
|
+
return renderToStaticMarkup(
|
|
39
|
+
<ParticipantThread
|
|
40
|
+
conversation={buildConversation({ subjectId: '1', subjectLabel: 'Invoice 4521' })}
|
|
41
|
+
items={ITEMS}
|
|
42
|
+
hasMore={false}
|
|
43
|
+
labels={DEFAULT_PARTICIPANT_CONVERSATIONS_LABELS}
|
|
44
|
+
resolveAttachmentUrl={async () => 'https://files.example/a'}
|
|
45
|
+
draft={{ value: '', onChange: () => undefined, files: [], onFilesChange: () => undefined }}
|
|
46
|
+
onSend={() => undefined}
|
|
47
|
+
status="ready"
|
|
48
|
+
refresh={() => undefined}
|
|
49
|
+
scroll={{ ref: { current: null }, onScroll: () => undefined }}
|
|
50
|
+
newMessagesCount={0}
|
|
51
|
+
isSending={false}
|
|
52
|
+
{...overrides}
|
|
53
|
+
/>,
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function mineFlags(markup: string): boolean[] {
|
|
58
|
+
return markup
|
|
59
|
+
.split('<div class="cv-p-bubble')
|
|
60
|
+
.slice(1)
|
|
61
|
+
.map((bubble) => bubble.startsWith(' cv-p-bubble--mine'))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
describe('ParticipantThread perspective', () => {
|
|
65
|
+
it('is untouched without the new props: the participant owns the inbound messages', () => {
|
|
66
|
+
expect(mineFlags(render())).toEqual([true, false, true])
|
|
67
|
+
expect(render({ perspective: 'participant' })).toBe(render())
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('draws none of the new header parts without the new props', () => {
|
|
71
|
+
const markup = render()
|
|
72
|
+
expect(markup).not.toContain('cv-p-thread__actions')
|
|
73
|
+
expect(markup).not.toContain('cv-p-thread__counterpart')
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('inverts which bubble is mine for the operator, keeping the own local pending message', () => {
|
|
77
|
+
expect(mineFlags(render({ perspective: 'operator' }))).toEqual([false, true, true])
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('moves the ticks to the operator side', () => {
|
|
81
|
+
const participant = render()
|
|
82
|
+
const operator = render({ perspective: 'operator' })
|
|
83
|
+
expect(participant).not.toContain('cv-status-ticks--read')
|
|
84
|
+
expect(operator).toContain('cv-status-ticks--read')
|
|
85
|
+
expect(operator).toContain('cv-status-ticks--queued')
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('puts the avatar on the counterpart, not on the operator', () => {
|
|
89
|
+
const participant = render({ avatars: 'initials' })
|
|
90
|
+
const operator = render({ perspective: 'operator', avatars: 'initials' })
|
|
91
|
+
expect(participant).not.toContain('>AS<')
|
|
92
|
+
expect(operator).toContain('class="cv-p-avatar" aria-hidden="true">AS<')
|
|
93
|
+
})
|
|
94
|
+
})
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
contrastRatio,
|
|
5
|
+
declaredToken,
|
|
6
|
+
defaultPalette,
|
|
7
|
+
fallbackOf,
|
|
8
|
+
flatten,
|
|
9
|
+
mappedPalette,
|
|
10
|
+
over,
|
|
11
|
+
parseColor,
|
|
12
|
+
tint,
|
|
13
|
+
type Palette,
|
|
14
|
+
type Rgba,
|
|
15
|
+
} from './participantContrast.test-helper'
|
|
16
|
+
|
|
17
|
+
const TEXT_MINIMUM = 4.5
|
|
18
|
+
const NON_TEXT_MINIMUM = 3
|
|
19
|
+
const WALLPAPER_SUBTLETY = 2
|
|
20
|
+
|
|
21
|
+
/** The light mapping a host panel can set on `.cv-p`; every other token keeps the package default. */
|
|
22
|
+
const PANEL_LIGHT_MAPPING = {
|
|
23
|
+
surface: '#f2efe9',
|
|
24
|
+
raised: '#ffffff',
|
|
25
|
+
text: '#1f2a30',
|
|
26
|
+
muted: '#59636a',
|
|
27
|
+
border: '#cfc8bb',
|
|
28
|
+
accent: '#a85a1c',
|
|
29
|
+
highlight: '#fbf3ea',
|
|
30
|
+
} as const
|
|
31
|
+
|
|
32
|
+
type Scenario = { readonly name: string; readonly palette: Palette; readonly tick: Rgba; readonly tickRead: Rgba }
|
|
33
|
+
|
|
34
|
+
function scenario(name: string, palette: Palette, theme: 'light' | 'dark'): Scenario {
|
|
35
|
+
const flat = flatten(palette)
|
|
36
|
+
const tick = flat.muted
|
|
37
|
+
const tickRead = parseColor(fallbackOf(declaredToken('--cv-p-i-tick-read', theme)))
|
|
38
|
+
return { name, palette: flat, tick, tickRead }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const SCENARIOS: readonly Scenario[] = [
|
|
42
|
+
scenario('light defaults', defaultPalette('light'), 'light'),
|
|
43
|
+
scenario('dark defaults', defaultPalette('dark'), 'dark'),
|
|
44
|
+
scenario('panel light mapping', mappedPalette(PANEL_LIGHT_MAPPING), 'light'),
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
function fillOf(palette: Palette, surface: 'received' | 'mine'): Rgba {
|
|
48
|
+
return surface === 'received' ? palette.raised : over(palette.highlight, palette.surface)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe.each(SCENARIOS.map((entry) => [entry.name, entry] as const))(
|
|
52
|
+
'contrast: %s',
|
|
53
|
+
(_name, { palette, tick, tickRead }) => {
|
|
54
|
+
it('keeps the text of both bubbles at 4.5:1 or more', () => {
|
|
55
|
+
for (const side of ['received', 'mine'] as const) {
|
|
56
|
+
const fill = fillOf(palette, side)
|
|
57
|
+
expect(contrastRatio(palette.text, fill)).toBeGreaterThanOrEqual(TEXT_MINIMUM)
|
|
58
|
+
expect(contrastRatio(palette.muted, fill)).toBeGreaterThanOrEqual(TEXT_MINIMUM)
|
|
59
|
+
expect(contrastRatio(palette.accent, fill)).toBeGreaterThanOrEqual(TEXT_MINIMUM)
|
|
60
|
+
expect(contrastRatio(palette.danger, fill)).toBeGreaterThanOrEqual(TEXT_MINIMUM)
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('keeps every tick at 3:1 or more on both bubbles', () => {
|
|
65
|
+
for (const side of ['received', 'mine'] as const) {
|
|
66
|
+
const fill = fillOf(palette, side)
|
|
67
|
+
expect(contrastRatio(tick, fill)).toBeGreaterThanOrEqual(NON_TEXT_MINIMUM)
|
|
68
|
+
expect(contrastRatio(tickRead, fill)).toBeGreaterThanOrEqual(NON_TEXT_MINIMUM)
|
|
69
|
+
expect(contrastRatio(palette.danger, fill)).toBeGreaterThanOrEqual(NON_TEXT_MINIMUM)
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('tells the own bubble from the page by an accent edge of 3:1 or more', () => {
|
|
74
|
+
expect(contrastRatio(palette.accent, palette.surface)).toBeGreaterThanOrEqual(NON_TEXT_MINIMUM)
|
|
75
|
+
expect(contrastRatio(palette.accent, fillOf(palette, 'mine'))).toBeGreaterThanOrEqual(NON_TEXT_MINIMUM)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('reads copyable tokens: text at 4.5:1 on the tint, the dotted underline at 3:1', () => {
|
|
79
|
+
for (const side of ['received', 'mine'] as const) {
|
|
80
|
+
const tinted = tint(palette.accent, 14, fillOf(palette, side))
|
|
81
|
+
expect(contrastRatio(palette.text, tinted)).toBeGreaterThanOrEqual(TEXT_MINIMUM)
|
|
82
|
+
expect(contrastRatio(palette.accent, fillOf(palette, side))).toBeGreaterThanOrEqual(NON_TEXT_MINIMUM)
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('lets text sit on the wallpaper dots and keeps the wallpaper itself subtle', () => {
|
|
87
|
+
const ink = palette.border
|
|
88
|
+
expect(contrastRatio(palette.text, ink)).toBeGreaterThanOrEqual(TEXT_MINIMUM)
|
|
89
|
+
expect(contrastRatio(palette.danger, palette.surface)).toBeGreaterThanOrEqual(TEXT_MINIMUM)
|
|
90
|
+
expect(contrastRatio(ink, palette.surface)).toBeLessThanOrEqual(WALLPAPER_SUBTLETY)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('keeps the strip above the composer and the unread badge readable', () => {
|
|
94
|
+
expect(contrastRatio(palette.accent, palette.raised)).toBeGreaterThanOrEqual(TEXT_MINIMUM)
|
|
95
|
+
expect(contrastRatio(palette.accentContrast, palette.accent)).toBeGreaterThanOrEqual(TEXT_MINIMUM)
|
|
96
|
+
})
|
|
97
|
+
},
|
|
98
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<div class="cv-p-root"><div class="cv-p-root__screen"><div class="cv-p cv-p-inbox" aria-busy="true"><h2 class="cv-p-inbox__title">Conversations</h2><div class="cv-p-loading"><span class="cv-p-skeleton" aria-hidden="true"></span><span class="cv-p-skeleton" aria-hidden="true"></span><span class="cv-p-sr-only">Loading…</span></div></div></div></div>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<div class="cv-p-root"><div class="cv-p-root__screen"><div class="cv-p cv-p-thread" aria-busy="true"></div></div></div>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<div class="cv-p cv-p-thread" aria-busy="false"><header class="cv-p-thread__header"><button type="button" class="cv-p-thread__back" aria-label="Back"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-left" aria-hidden="true" focusable="false"><path d="m12 19-7-7 7-7"></path><path d="M19 12H5"></path></svg></button><div class="cv-p-thread__heading"><h2 class="cv-p-thread__title"><span class="cv-p-thread__title-text">Invoice 4521</span></h2><div class="cv-p-thread__meta"><span class="cv-p-protocol cv-p-protocol--header"><span class="cv-p-protocol__code"><span class="cv-p-sr-only">Protocol </span>P-1</span><button type="button" class="cv-p-protocol__copy" aria-label="Copy protocol P-1"><svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-copy" aria-hidden="true"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"></rect><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"></path></svg></button></span><div class="cv-p-sr-only" role="status" aria-live="polite"></div><span class="cv-p-channels"><span class="cv-p-sr-only">Channels in this conversation: </span><span class="cv-p-channel cv-p-channel--app"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-smartphone" aria-hidden="true"><rect width="14" height="20" x="5" y="2" rx="2" ry="2"></rect><path d="M12 18h.01"></path></svg><span class="cv-p-sr-only">App, </span></span><span class="cv-p-channel cv-p-channel--whatsapp"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-message-circle" aria-hidden="true"><path d="M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719"></path></svg><span class="cv-p-sr-only">WhatsApp</span></span></span></div></div></header><div class="cv-p-thread__scroll" tabindex="-1"><button type="button" class="cv-p-thread__older">Load older messages</button><div class="cv-p-thread__messages"><div class="cv-p-day" role="separator" aria-label="Thu, 10/01"><time class="cv-p-day__label" dateTime="2026-10-01">Thu, 10/01</time></div><div class="cv-p-bubble cv-p-bubble--mine"><span class="cv-p-sr-only">Me</span><div class="cv-p-text">*<strong>oi</strong>* <button type="button" class="cv-p-copyable cv-p-copyable--phone" aria-label="Copy 11 98888-7777">11 98888-7777</button> <button type="button" class="cv-p-copyable cv-p-copyable--email" aria-label="Copy a@b.com">a@b.com</button> <span class="cv-p-text__link-group"><a class="cv-p-text__link" href="https://x.com" rel="noopener noreferrer nofollow ugc" target="_blank">https://x.com<span class="cv-p-sr-only"> (opens in a new tab)</span></a><button type="button" class="cv-p-text__link-copy" aria-label="Copy https://x.com"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-copy" aria-hidden="true"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"></rect><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"></path></svg></button></span></div><span class="cv-p-bubble__meta"><time class="cv-p-bubble__time" dateTime="2026-10-01T12:00:00.000Z">12:00</time><span class="cv-p-bubble__status"><span class="cv-status-ticks cv-status-ticks--sent"><svg viewBox="0 0 20 12" width="15" height="9" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M1 6.5L5 10.5L14.5 1" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"></path></svg></span><span class="cv-p-sr-only">Sent</span></span></span></div><div class="cv-p-bubble-row"><span class="cv-p-bubble-row__avatar"><span class="cv-p-avatar" aria-hidden="true">O</span></span><div class="cv-p-bubble"><span class="cv-p-bubble__author">Op</span><div class="cv-p-text">Hello</div><span class="cv-p-bubble__meta"><time class="cv-p-bubble__time" dateTime="2026-10-01T12:05:00.000Z">12:05</time></span></div></div><div class="cv-p-day" role="separator" aria-label="Fri, 10/02"><time class="cv-p-day__label" dateTime="2026-10-02">Fri, 10/02</time></div><div class="cv-p-bubble cv-p-bubble--mine"><span class="cv-p-sr-only">Me</span><div class="cv-p-text">Hello</div><span class="cv-p-bubble__meta"><time class="cv-p-bubble__time" dateTime="2026-10-02T12:05:00.000Z">12:05</time><span class="cv-p-bubble__status"><span class="cv-status-ticks cv-status-ticks--delivered"><svg viewBox="0 0 20 12" width="15" height="9" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M1 6.5L4.5 10L11 2" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"></path><path d="M6 6.5L9.5 10L19 1" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"></path></svg></span><span class="cv-p-sr-only">Delivered</span></span></span></div><div class="cv-p-bubble cv-p-bubble--mine"><span class="cv-p-sr-only">Me</span><div class="cv-p-text">W</div><span class="cv-p-bubble__meta"><time class="cv-p-bubble__time" dateTime="2026-10-02T12:06:00.000Z">12:06</time><button type="button" class="cv-p-bubble__retry"><span class="cv-status-ticks cv-status-ticks--failed"><svg xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-triangle-alert lucide-alert-triangle" aria-hidden="true"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"></path><path d="M12 9v4"></path><path d="M12 17h.01"></path></svg></span><span>Failed — tap to retry</span></button></span><span class="cv-p-bubble__actions"><button type="button" class="cv-p-bubble__action">Edit</button><button type="button" class="cv-p-bubble__action">Discard</button></span></div></div><div class="cv-p-thread__latest"><button type="button" class="cv-p-thread__latest-button" aria-label="Scroll to latest message"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-down" aria-hidden="true" focusable="false"><path d="m6 9 6 6 6-6"></path></svg><span class="cv-p-thread__latest-badge" aria-hidden="true">2</span></button></div></div><div class="cv-p-thread__live" role="status" aria-live="polite">New messages</div><form class="cv-p-composer"><div class="cv-p-quick" role="group" aria-label="Quick replies"><button type="button" class="cv-p-chip">T</button></div><div class="cv-p-composer__row"><input class="cv-p-composer__file-input" type="file" multiple="" hidden=""/><button type="button" class="cv-p-composer__attach" aria-label="Attach"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-paperclip" aria-hidden="true" focusable="false"><path d="m16 6-8.414 8.586a2 2 0 0 0 2.829 2.829l8.414-8.586a4 4 0 1 0-5.657-5.657l-8.379 8.551a6 6 0 1 0 8.485 8.485l8.379-8.551"></path></svg></button><textarea class="cv-p-composer__input" aria-label="Message" placeholder="Write a message…" rows="1">abc</textarea><button type="submit" class="cv-p-button cv-p-button--primary cv-p-composer__send" aria-label="Send"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-send" aria-hidden="true" focusable="false"><path d="M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z"></path><path d="m21.854 2.147-10.94 10.939"></path></svg></button></div></form></div>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<div class="cv-p cv-p-thread" aria-busy="false"><header class="cv-p-thread__header"><button type="button" class="cv-p-thread__back" aria-label="Back"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-left" aria-hidden="true" focusable="false"><path d="m12 19-7-7 7-7"></path><path d="M19 12H5"></path></svg></button><div class="cv-p-thread__heading"><h2 class="cv-p-thread__title"><span class="cv-p-thread__title-text">Invoice 4521</span></h2></div></header><div class="cv-p-thread__scroll" tabindex="-1"><button type="button" class="cv-p-thread__older">Load older messages</button><div class="cv-p-thread__messages"><div class="cv-p-day" role="separator" aria-label="Thu, 10/01"><time class="cv-p-day__label" dateTime="2026-10-01">Thu, 10/01</time></div><div class="cv-p-bubble cv-p-bubble--mine"><span class="cv-p-sr-only">Me</span><div class="cv-p-text">*<strong>oi</strong>* <button type="button" class="cv-p-copyable cv-p-copyable--phone" aria-label="Copy 11 98888-7777">11 98888-7777</button> <button type="button" class="cv-p-copyable cv-p-copyable--email" aria-label="Copy a@b.com">a@b.com</button> <span class="cv-p-text__link-group"><a class="cv-p-text__link" href="https://x.com" rel="noopener noreferrer nofollow ugc" target="_blank">https://x.com<span class="cv-p-sr-only"> (opens in a new tab)</span></a><button type="button" class="cv-p-text__link-copy" aria-label="Copy https://x.com"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-copy" aria-hidden="true"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"></rect><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"></path></svg></button></span></div><span class="cv-p-bubble__meta"><time class="cv-p-bubble__time" dateTime="2026-10-01T12:00:00.000Z">12:00</time><span class="cv-p-bubble__status"><span class="cv-status-ticks cv-status-ticks--sent"><svg viewBox="0 0 20 12" width="15" height="9" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M1 6.5L5 10.5L14.5 1" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"></path></svg></span><span class="cv-p-sr-only">Sent</span></span></span></div><div class="cv-p-bubble"><span class="cv-p-bubble__author">Op</span><div class="cv-p-text">Hello</div><span class="cv-p-bubble__meta"><time class="cv-p-bubble__time" dateTime="2026-10-01T12:05:00.000Z">12:05</time></span></div><div class="cv-p-day" role="separator" aria-label="Fri, 10/02"><time class="cv-p-day__label" dateTime="2026-10-02">Fri, 10/02</time></div><div class="cv-p-bubble cv-p-bubble--mine"><span class="cv-p-sr-only">Me</span><div class="cv-p-text">Hello</div><span class="cv-p-bubble__meta"><time class="cv-p-bubble__time" dateTime="2026-10-02T12:05:00.000Z">12:05</time><span class="cv-p-bubble__status"><span class="cv-status-ticks cv-status-ticks--delivered"><svg viewBox="0 0 20 12" width="15" height="9" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M1 6.5L4.5 10L11 2" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"></path><path d="M6 6.5L9.5 10L19 1" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"></path></svg></span><span class="cv-p-sr-only">Delivered</span></span></span></div><div class="cv-p-bubble cv-p-bubble--mine"><span class="cv-p-sr-only">Me</span><div class="cv-p-text">W</div><span class="cv-p-bubble__meta"><time class="cv-p-bubble__time" dateTime="2026-10-02T12:06:00.000Z">12:06</time><button type="button" class="cv-p-bubble__retry"><span class="cv-status-ticks cv-status-ticks--failed"><svg xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-triangle-alert lucide-alert-triangle" aria-hidden="true"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"></path><path d="M12 9v4"></path><path d="M12 17h.01"></path></svg></span><span>Failed — tap to retry</span></button></span><span class="cv-p-bubble__actions"><button type="button" class="cv-p-bubble__action">Edit</button><button type="button" class="cv-p-bubble__action">Discard</button></span></div></div><div class="cv-p-thread__latest"><button type="button" class="cv-p-thread__latest-button" aria-label="Scroll to latest message"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-down" aria-hidden="true" focusable="false"><path d="m6 9 6 6 6-6"></path></svg><span class="cv-p-thread__latest-badge" aria-hidden="true">2</span></button></div></div><div class="cv-p-thread__live" role="status" aria-live="polite">New messages</div><p class="cv-p-thread__closed">This conversation is closed</p></div>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<div class="cv-p cv-p-thread" aria-busy="false"><header class="cv-p-thread__header"><button type="button" class="cv-p-thread__back" aria-label="Back"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-left" aria-hidden="true" focusable="false"><path d="m12 19-7-7 7-7"></path><path d="M19 12H5"></path></svg></button><div class="cv-p-thread__heading"><h2 class="cv-p-thread__title"><span class="cv-p-thread__title-text">Invoice 4521</span></h2><div class="cv-p-thread__meta"><span class="cv-p-protocol cv-p-protocol--header"><span class="cv-p-protocol__code"><span class="cv-p-sr-only">Protocol </span>P-1</span><button type="button" class="cv-p-protocol__copy" aria-label="Copy protocol P-1"><svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-copy" aria-hidden="true"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"></rect><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"></path></svg></button></span><div class="cv-p-sr-only" role="status" aria-live="polite"></div><span class="cv-p-channels"><span class="cv-p-sr-only">Channels in this conversation: </span><span class="cv-p-channel cv-p-channel--app"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-smartphone" aria-hidden="true"><rect width="14" height="20" x="5" y="2" rx="2" ry="2"></rect><path d="M12 18h.01"></path></svg><span class="cv-p-sr-only">App, </span></span><span class="cv-p-channel cv-p-channel--whatsapp"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-message-circle" aria-hidden="true"><path d="M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719"></path></svg><span class="cv-p-sr-only">WhatsApp</span></span></span></div></div></header><div class="cv-p-thread__scroll" tabindex="-1"><button type="button" class="cv-p-thread__older">Load older messages</button><div class="cv-p-thread__messages"><div class="cv-p-day" role="separator" aria-label="Thu, 10/01"><time class="cv-p-day__label" dateTime="2026-10-01">Thu, 10/01</time></div><div class="cv-p-bubble cv-p-bubble--mine"><span class="cv-p-sr-only">Me</span><div class="cv-p-text">*<strong>oi</strong>* <button type="button" class="cv-p-copyable cv-p-copyable--phone" aria-label="Copy 11 98888-7777">11 98888-7777</button> <button type="button" class="cv-p-copyable cv-p-copyable--email" aria-label="Copy a@b.com">a@b.com</button> <span class="cv-p-text__link-group"><a class="cv-p-text__link" href="https://x.com" rel="noopener noreferrer nofollow ugc" target="_blank">https://x.com<span class="cv-p-sr-only"> (opens in a new tab)</span></a><button type="button" class="cv-p-text__link-copy" aria-label="Copy https://x.com"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-copy" aria-hidden="true"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"></rect><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"></path></svg></button></span></div><span class="cv-p-bubble__meta"><time class="cv-p-bubble__time" dateTime="2026-10-01T12:00:00.000Z">12:00</time><span class="cv-p-bubble__status"><span class="cv-status-ticks cv-status-ticks--sent"><svg viewBox="0 0 20 12" width="15" height="9" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M1 6.5L5 10.5L14.5 1" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"></path></svg></span><span class="cv-p-sr-only">Sent</span></span></span></div><div class="cv-p-bubble"><span class="cv-p-bubble__author">Op</span><div class="cv-p-text">Hello</div><span class="cv-p-bubble__meta"><time class="cv-p-bubble__time" dateTime="2026-10-01T12:05:00.000Z">12:05</time></span></div><div class="cv-p-day" role="separator" aria-label="Fri, 10/02"><time class="cv-p-day__label" dateTime="2026-10-02">Fri, 10/02</time></div><div class="cv-p-bubble cv-p-bubble--mine"><span class="cv-p-sr-only">Me</span><div class="cv-p-text">Hello</div><span class="cv-p-bubble__meta"><time class="cv-p-bubble__time" dateTime="2026-10-02T12:05:00.000Z">12:05</time><span class="cv-p-bubble__status"><span class="cv-status-ticks cv-status-ticks--delivered"><svg viewBox="0 0 20 12" width="15" height="9" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M1 6.5L4.5 10L11 2" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"></path><path d="M6 6.5L9.5 10L19 1" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"></path></svg></span><span class="cv-p-sr-only">Delivered</span></span></span></div><div class="cv-p-bubble cv-p-bubble--mine"><span class="cv-p-sr-only">Me</span><div class="cv-p-text">W</div><span class="cv-p-bubble__meta"><time class="cv-p-bubble__time" dateTime="2026-10-02T12:06:00.000Z">12:06</time><button type="button" class="cv-p-bubble__retry"><span class="cv-status-ticks cv-status-ticks--failed"><svg xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-triangle-alert lucide-alert-triangle" aria-hidden="true"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"></path><path d="M12 9v4"></path><path d="M12 17h.01"></path></svg></span><span>Failed — tap to retry</span></button></span><span class="cv-p-bubble__actions"><button type="button" class="cv-p-bubble__action">Edit</button><button type="button" class="cv-p-bubble__action">Discard</button></span></div></div><div class="cv-p-thread__latest"><button type="button" class="cv-p-thread__latest-button" aria-label="Scroll to latest message"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-down" aria-hidden="true" focusable="false"><path d="m6 9 6 6 6-6"></path></svg><span class="cv-p-thread__latest-badge" aria-hidden="true">2</span></button></div></div><div class="cv-p-thread__live" role="status" aria-live="polite">New messages</div><form class="cv-p-composer"><div class="cv-p-quick" role="group" aria-label="Quick replies"><button type="button" class="cv-p-chip">T</button></div><div class="cv-p-composer__row"><input class="cv-p-composer__file-input" type="file" multiple="" hidden=""/><button type="button" class="cv-p-composer__attach" aria-label="Attach"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-paperclip" aria-hidden="true" focusable="false"><path d="m16 6-8.414 8.586a2 2 0 0 0 2.829 2.829l8.414-8.586a4 4 0 1 0-5.657-5.657l-8.379 8.551a6 6 0 1 0 8.485 8.485l8.379-8.551"></path></svg></button><textarea class="cv-p-composer__input" aria-label="Message" placeholder="Write a message…" rows="1">abc</textarea><button type="submit" class="cv-p-button cv-p-button--primary cv-p-composer__send" aria-label="Send"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-send" aria-hidden="true" focusable="false"><path d="M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z"></path><path d="m21.854 2.147-10.94 10.939"></path></svg></button></div></form></div>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<div class="cv-p cv-p-thread" aria-busy="false"><header class="cv-p-thread__header"><button type="button" class="cv-p-thread__back" aria-label="Back"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-left" aria-hidden="true" focusable="false"><path d="m12 19-7-7 7-7"></path><path d="M19 12H5"></path></svg></button><div class="cv-p-thread__heading"><h2 class="cv-p-thread__title"><span class="cv-p-thread__title-text">Invoice 4521</span></h2><div class="cv-p-thread__meta"><span class="cv-p-protocol cv-p-protocol--header"><span class="cv-p-protocol__code"><span class="cv-p-sr-only">Protocol </span>P-1</span><button type="button" class="cv-p-protocol__copy" aria-label="Copy protocol P-1"><svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-copy" aria-hidden="true"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"></rect><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"></path></svg></button></span><div class="cv-p-sr-only" role="status" aria-live="polite"></div><span class="cv-p-channels"><span class="cv-p-sr-only">Channels in this conversation: </span><span class="cv-p-channel cv-p-channel--app"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-smartphone" aria-hidden="true"><rect width="14" height="20" x="5" y="2" rx="2" ry="2"></rect><path d="M12 18h.01"></path></svg><span class="cv-p-sr-only">App, </span></span><span class="cv-p-channel cv-p-channel--whatsapp"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-message-circle" aria-hidden="true"><path d="M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719"></path></svg><span class="cv-p-sr-only">WhatsApp</span></span></span></div></div></header><div class="cv-p-thread__scroll" tabindex="-1"><div class="cv-p-error" role="alert"><p class="cv-p-error__message">Could not load. Check your connection and try again.</p><button type="button" class="cv-p-button">Retry</button></div><button type="button" class="cv-p-thread__older">Load older messages</button><div class="cv-p-thread__messages"></div></div><div class="cv-p-thread__live" role="status" aria-live="polite">New messages</div><form class="cv-p-composer"><div class="cv-p-quick" role="group" aria-label="Quick replies"><button type="button" class="cv-p-chip">T</button></div><div class="cv-p-composer__row"><input class="cv-p-composer__file-input" type="file" multiple="" hidden=""/><button type="button" class="cv-p-composer__attach" aria-label="Attach"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-paperclip" aria-hidden="true" focusable="false"><path d="m16 6-8.414 8.586a2 2 0 0 0 2.829 2.829l8.414-8.586a4 4 0 1 0-5.657-5.657l-8.379 8.551a6 6 0 1 0 8.485 8.485l8.379-8.551"></path></svg></button><textarea class="cv-p-composer__input" aria-label="Message" placeholder="Write a message…" rows="1">abc</textarea><button type="submit" class="cv-p-button cv-p-button--primary cv-p-composer__send" aria-label="Send"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-send" aria-hidden="true" focusable="false"><path d="M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z"></path><path d="m21.854 2.147-10.94 10.939"></path></svg></button></div></form></div>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<div class="cv-p cv-p-thread" aria-busy="true"><header class="cv-p-thread__header"><button type="button" class="cv-p-thread__back" aria-label="Back"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-left" aria-hidden="true" focusable="false"><path d="m12 19-7-7 7-7"></path><path d="M19 12H5"></path></svg></button><div class="cv-p-thread__heading"><h2 class="cv-p-thread__title"><span class="cv-p-thread__title-text">Invoice 4521</span></h2><div class="cv-p-thread__meta"><span class="cv-p-protocol cv-p-protocol--header"><span class="cv-p-protocol__code"><span class="cv-p-sr-only">Protocol </span>P-1</span><button type="button" class="cv-p-protocol__copy" aria-label="Copy protocol P-1"><svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-copy" aria-hidden="true"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"></rect><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"></path></svg></button></span><div class="cv-p-sr-only" role="status" aria-live="polite"></div><span class="cv-p-channels"><span class="cv-p-sr-only">Channels in this conversation: </span><span class="cv-p-channel cv-p-channel--app"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-smartphone" aria-hidden="true"><rect width="14" height="20" x="5" y="2" rx="2" ry="2"></rect><path d="M12 18h.01"></path></svg><span class="cv-p-sr-only">App, </span></span><span class="cv-p-channel cv-p-channel--whatsapp"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-message-circle" aria-hidden="true"><path d="M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719"></path></svg><span class="cv-p-sr-only">WhatsApp</span></span></span></div></div></header><div class="cv-p-thread__scroll" tabindex="-1"><div class="cv-p-loading"><span class="cv-p-skeleton" aria-hidden="true"></span><span class="cv-p-skeleton" aria-hidden="true"></span><span class="cv-p-sr-only">Loading…</span></div><button type="button" class="cv-p-thread__older">Load older messages</button><div class="cv-p-thread__messages"></div></div><div class="cv-p-thread__live" role="status" aria-live="polite">New messages</div><form class="cv-p-composer"><div class="cv-p-quick" role="group" aria-label="Quick replies"><button type="button" class="cv-p-chip">T</button></div><div class="cv-p-composer__row"><input class="cv-p-composer__file-input" type="file" multiple="" hidden=""/><button type="button" class="cv-p-composer__attach" aria-label="Attach"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-paperclip" aria-hidden="true" focusable="false"><path d="m16 6-8.414 8.586a2 2 0 0 0 2.829 2.829l8.414-8.586a4 4 0 1 0-5.657-5.657l-8.379 8.551a6 6 0 1 0 8.485 8.485l8.379-8.551"></path></svg></button><textarea class="cv-p-composer__input" aria-label="Message" placeholder="Write a message…" rows="1">abc</textarea><button type="submit" class="cv-p-button cv-p-button--primary cv-p-composer__send" aria-label="Send"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-send" aria-hidden="true" focusable="false"><path d="M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z"></path><path d="m21.854 2.147-10.94 10.939"></path></svg></button></div></form></div>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<div class="cv-p cv-p-thread" aria-busy="false"><header class="cv-p-thread__header"><button type="button" class="cv-p-thread__back" aria-label="Back"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-left" aria-hidden="true" focusable="false"><path d="m12 19-7-7 7-7"></path><path d="M19 12H5"></path></svg></button><div class="cv-p-thread__heading"><h2 class="cv-p-thread__title"><span class="cv-p-thread__title-text">Invoice 4521</span></h2><div class="cv-p-thread__meta"><span class="cv-p-protocol cv-p-protocol--header"><span class="cv-p-protocol__code"><span class="cv-p-sr-only">Protocol </span>P-1</span><button type="button" class="cv-p-protocol__copy" aria-label="Copy protocol P-1"><svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-copy" aria-hidden="true"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"></rect><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"></path></svg></button></span><div class="cv-p-sr-only" role="status" aria-live="polite"></div><span class="cv-p-channels"><span class="cv-p-sr-only">Channels in this conversation: </span><span class="cv-p-channel cv-p-channel--app"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-smartphone" aria-hidden="true"><rect width="14" height="20" x="5" y="2" rx="2" ry="2"></rect><path d="M12 18h.01"></path></svg><span class="cv-p-sr-only">App, </span></span><span class="cv-p-channel cv-p-channel--whatsapp"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-message-circle" aria-hidden="true"><path d="M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719"></path></svg><span class="cv-p-sr-only">WhatsApp</span></span></span></div></div></header><div class="cv-p-thread__scroll" tabindex="-1"><div class="cv-p-thread__messages"><div class="cv-p-day" role="separator" aria-label="Thu, 10/01"><time class="cv-p-day__label" dateTime="2026-10-01">Thu, 10/01</time></div><div class="cv-p-bubble cv-p-bubble--mine"><span class="cv-p-sr-only">Me</span><div class="cv-p-text">*<strong>oi</strong>* <button type="button" class="cv-p-copyable cv-p-copyable--phone" aria-label="Copy 11 98888-7777">11 98888-7777</button> <button type="button" class="cv-p-copyable cv-p-copyable--email" aria-label="Copy a@b.com">a@b.com</button> <span class="cv-p-text__link-group"><a class="cv-p-text__link" href="https://x.com" rel="noopener noreferrer nofollow ugc" target="_blank">https://x.com<span class="cv-p-sr-only"> (opens in a new tab)</span></a><button type="button" class="cv-p-text__link-copy" aria-label="Copy https://x.com"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-copy" aria-hidden="true"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"></rect><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"></path></svg></button></span></div><span class="cv-p-bubble__meta"><time class="cv-p-bubble__time" dateTime="2026-10-01T12:00:00.000Z">12:00</time><span class="cv-p-bubble__status"><span class="cv-status-ticks cv-status-ticks--sent"><svg viewBox="0 0 20 12" width="15" height="9" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M1 6.5L5 10.5L14.5 1" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"></path></svg></span><span class="cv-p-sr-only">Sent</span></span></span></div><div class="cv-p-bubble"><span class="cv-p-bubble__author">Op</span><div class="cv-p-text">Hello</div><span class="cv-p-bubble__meta"><time class="cv-p-bubble__time" dateTime="2026-10-01T12:05:00.000Z">12:05</time></span></div></div></div><div class="cv-p-thread__live" role="status" aria-live="polite"></div><form class="cv-p-composer"><div class="cv-p-composer__row"><input class="cv-p-composer__file-input" type="file" multiple="" hidden=""/><button type="button" class="cv-p-composer__attach" aria-label="Attach"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-paperclip" aria-hidden="true" focusable="false"><path d="m16 6-8.414 8.586a2 2 0 0 0 2.829 2.829l8.414-8.586a4 4 0 1 0-5.657-5.657l-8.379 8.551a6 6 0 1 0 8.485 8.485l8.379-8.551"></path></svg></button><textarea class="cv-p-composer__input" aria-label="Message" placeholder="Write a message…" rows="1">abc</textarea><button type="submit" class="cv-p-button cv-p-button--primary cv-p-composer__send" aria-label="Send"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-send" aria-hidden="true" focusable="false"><path d="M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z"></path><path d="m21.854 2.147-10.94 10.939"></path></svg></button></div></form></div>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<div class="cv-p cv-p-thread" aria-busy="false"><header class="cv-p-thread__header"><button type="button" class="cv-p-thread__back" aria-label="Back"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-left" aria-hidden="true" focusable="false"><path d="m12 19-7-7 7-7"></path><path d="M19 12H5"></path></svg></button><div class="cv-p-thread__heading"><h2 class="cv-p-thread__title"><span class="cv-p-thread__title-text">Invoice 4521</span></h2><div class="cv-p-thread__meta"><span class="cv-p-protocol cv-p-protocol--header"><span class="cv-p-protocol__code"><span class="cv-p-sr-only">Protocol </span>P-1</span><button type="button" class="cv-p-protocol__copy" aria-label="Copy protocol P-1"><svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-copy" aria-hidden="true"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"></rect><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"></path></svg></button></span><div class="cv-p-sr-only" role="status" aria-live="polite"></div><span class="cv-p-channels"><span class="cv-p-sr-only">Channels in this conversation: </span><span class="cv-p-channel cv-p-channel--app"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-smartphone" aria-hidden="true"><rect width="14" height="20" x="5" y="2" rx="2" ry="2"></rect><path d="M12 18h.01"></path></svg><span class="cv-p-sr-only">App, </span></span><span class="cv-p-channel cv-p-channel--whatsapp"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-message-circle" aria-hidden="true"><path d="M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719"></path></svg><span class="cv-p-sr-only">WhatsApp</span></span></span></div></div></header><div class="cv-p-thread__scroll" tabindex="-1"><button type="button" class="cv-p-thread__older">Load older messages</button><div class="cv-p-thread__messages"><div class="cv-p-day" role="separator" aria-label="Thu, 10/01"><time class="cv-p-day__label" dateTime="2026-10-01">Thu, 10/01</time></div><div class="cv-p-bubble cv-p-bubble--mine cv-p-bubble--no-tail"><span class="cv-p-sr-only">Me</span><div class="cv-p-text">*<strong>oi</strong>* <button type="button" class="cv-p-copyable cv-p-copyable--phone" aria-label="Copy 11 98888-7777">11 98888-7777</button> <button type="button" class="cv-p-copyable cv-p-copyable--email" aria-label="Copy a@b.com">a@b.com</button> <span class="cv-p-text__link-group"><a class="cv-p-text__link" href="https://x.com" rel="noopener noreferrer nofollow ugc" target="_blank">https://x.com<span class="cv-p-sr-only"> (opens in a new tab)</span></a><button type="button" class="cv-p-text__link-copy" aria-label="Copy https://x.com"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-copy" aria-hidden="true"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"></rect><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"></path></svg></button></span></div><span class="cv-p-bubble__meta"><time class="cv-p-bubble__time" dateTime="2026-10-01T12:00:00.000Z">12:00</time><span class="cv-p-bubble__status"><span class="cv-status-ticks cv-status-ticks--sent"><svg viewBox="0 0 20 12" width="15" height="9" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M1 6.5L5 10.5L14.5 1" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"></path></svg></span><span class="cv-p-sr-only">Sent</span></span></span></div><div class="cv-p-bubble cv-p-bubble--no-tail"><span class="cv-p-bubble__author">Op</span><div class="cv-p-text">Hello</div><span class="cv-p-bubble__meta"><time class="cv-p-bubble__time" dateTime="2026-10-01T12:05:00.000Z">12:05</time></span></div><div class="cv-p-day" role="separator" aria-label="Fri, 10/02"><time class="cv-p-day__label" dateTime="2026-10-02">Fri, 10/02</time></div><div class="cv-p-bubble cv-p-bubble--mine cv-p-bubble--no-tail"><span class="cv-p-sr-only">Me</span><div class="cv-p-text">Hello</div><span class="cv-p-bubble__meta"><time class="cv-p-bubble__time" dateTime="2026-10-02T12:05:00.000Z">12:05</time><span class="cv-p-bubble__status"><span class="cv-status-ticks cv-status-ticks--delivered"><svg viewBox="0 0 20 12" width="15" height="9" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M1 6.5L4.5 10L11 2" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"></path><path d="M6 6.5L9.5 10L19 1" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"></path></svg></span><span class="cv-p-sr-only">Delivered</span></span></span></div><div class="cv-p-bubble cv-p-bubble--mine cv-p-bubble--no-tail"><span class="cv-p-sr-only">Me</span><div class="cv-p-text">W</div><span class="cv-p-bubble__meta"><time class="cv-p-bubble__time" dateTime="2026-10-02T12:06:00.000Z">12:06</time><button type="button" class="cv-p-bubble__retry"><span class="cv-status-ticks cv-status-ticks--failed"><svg xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-triangle-alert lucide-alert-triangle" aria-hidden="true"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"></path><path d="M12 9v4"></path><path d="M12 17h.01"></path></svg></span><span>Failed — tap to retry</span></button></span><span class="cv-p-bubble__actions"><button type="button" class="cv-p-bubble__action">Edit</button><button type="button" class="cv-p-bubble__action">Discard</button></span></div></div><div class="cv-p-thread__latest"><button type="button" class="cv-p-thread__latest-button" aria-label="Scroll to latest message"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-down" aria-hidden="true" focusable="false"><path d="m6 9 6 6 6-6"></path></svg><span class="cv-p-thread__latest-badge" aria-hidden="true">2</span></button></div></div><div class="cv-p-thread__live" role="status" aria-live="polite">New messages</div><form class="cv-p-composer"><div class="cv-p-quick" role="group" aria-label="Quick replies"><button type="button" class="cv-p-chip">T</button></div><div class="cv-p-composer__row"><input class="cv-p-composer__file-input" type="file" multiple="" hidden=""/><button type="button" class="cv-p-composer__attach" aria-label="Attach"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-paperclip" aria-hidden="true" focusable="false"><path d="m16 6-8.414 8.586a2 2 0 0 0 2.829 2.829l8.414-8.586a4 4 0 1 0-5.657-5.657l-8.379 8.551a6 6 0 1 0 8.485 8.485l8.379-8.551"></path></svg></button><textarea class="cv-p-composer__input" aria-label="Message" placeholder="Write a message…" rows="1">abc</textarea><button type="submit" class="cv-p-button cv-p-button--primary cv-p-composer__send" aria-label="Send"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-send" aria-hidden="true" focusable="false"><path d="M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z"></path><path d="m21.854 2.147-10.94 10.939"></path></svg></button></div></form></div>
|
|
@@ -4,6 +4,7 @@ import type { ParticipantConversationSummary, ParticipantSubjectRef } from '@ada
|
|
|
4
4
|
|
|
5
5
|
import type { ConversationChannel } from '../conversationChannel'
|
|
6
6
|
import type { ParticipantConversationsApi, ParticipantPendingMessage } from './participantApi.types'
|
|
7
|
+
import type { ParticipantPerspective } from './participantPerspective'
|
|
7
8
|
import { mergeParticipantMessages, type ParticipantTimelineItem } from './participantMessages'
|
|
8
9
|
import type { ParticipantSendAction, ParticipantSendEntry } from './participantSendController'
|
|
9
10
|
import { selectSendEntries, type ParticipantSendStates, type ParticipantSendStatesAction } from './participantSendStates'
|
|
@@ -18,6 +19,7 @@ export type UseParticipantConversationViewParams = {
|
|
|
18
19
|
readonly pendingMessages?: readonly ParticipantPendingMessage[]
|
|
19
20
|
readonly sendStates: ParticipantSendStates
|
|
20
21
|
readonly dispatchSendStates: Dispatch<ParticipantSendStatesAction>
|
|
22
|
+
readonly perspective?: ParticipantPerspective
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
export type UseParticipantConversationViewResult = {
|
|
@@ -44,6 +46,7 @@ export function useParticipantConversationView(params: UseParticipantConversatio
|
|
|
44
46
|
sendEntries,
|
|
45
47
|
dispatchSend,
|
|
46
48
|
hostPending: pendingMessages,
|
|
49
|
+
perspective: params.perspective,
|
|
47
50
|
})
|
|
48
51
|
const items = useMemo(
|
|
49
52
|
() =>
|
|
@@ -17,9 +17,11 @@ import {
|
|
|
17
17
|
type ParticipantSendEntry,
|
|
18
18
|
} from './participantSendController'
|
|
19
19
|
import { INITIAL_PARTICIPANT_THREAD_STATE, participantThreadReducer, type ParticipantThreadState } from './participantThreadState'
|
|
20
|
+
import type { ParticipantPerspective } from './participantPerspective'
|
|
20
21
|
import { useParticipantMarkRead } from './useParticipantMarkRead'
|
|
21
22
|
import { useParticipantRevalidation } from './useParticipantRevalidation'
|
|
22
23
|
import { useParticipantSend, type UseParticipantSendResult } from './useParticipantSend'
|
|
24
|
+
import { useParticipantUnread } from './useParticipantUnread'
|
|
23
25
|
|
|
24
26
|
const THREAD_PAGE_SIZE = 30
|
|
25
27
|
|
|
@@ -35,6 +37,8 @@ export type UseParticipantThreadParams = {
|
|
|
35
37
|
readonly sendEntries: readonly ParticipantSendEntry[]
|
|
36
38
|
readonly dispatchSend: (action: ParticipantSendAction) => void
|
|
37
39
|
readonly hostPending?: readonly ParticipantPendingMessage[]
|
|
40
|
+
/** Present on a single-conversation screen: unread comes from the messages, not from `unreadCount`. */
|
|
41
|
+
readonly perspective?: ParticipantPerspective
|
|
38
42
|
}
|
|
39
43
|
|
|
40
44
|
export type UseParticipantThreadResult = ParticipantThreadState &
|
|
@@ -55,7 +59,7 @@ function isForSubject(event: ParticipantConversationEvent | undefined, subject:
|
|
|
55
59
|
}
|
|
56
60
|
|
|
57
61
|
export function useParticipantThread(params: UseParticipantThreadParams): UseParticipantThreadResult {
|
|
58
|
-
const { api, subject, channel, unreadCount = 0, onMarkedRead, sendEntries, dispatchSend, hostPending = [] } = params
|
|
62
|
+
const { api, subject, channel, unreadCount = 0, onMarkedRead, sendEntries, dispatchSend, hostPending = [], perspective } = params
|
|
59
63
|
const { subjectType, subjectId } = subject
|
|
60
64
|
const stableSubject = useMemo<ParticipantSubjectRef>(() => ({ subjectType, subjectId }), [subjectType, subjectId])
|
|
61
65
|
const [state, dispatch] = useReducer(participantThreadReducer, INITIAL_PARTICIPANT_THREAD_STATE)
|
|
@@ -109,7 +113,14 @@ export function useParticipantThread(params: UseParticipantThreadParams): UsePar
|
|
|
109
113
|
dispatchSend({ type: 'reflected', knownClientMessageIds: collectKnownClientMessageIds({ messages: state.messages, hostPending }) })
|
|
110
114
|
}, [sendEntries, state.messages, hostPending, dispatchSend])
|
|
111
115
|
|
|
112
|
-
|
|
116
|
+
const unread = useParticipantUnread({ messages: state.messages, perspective, reportedUnreadCount: unreadCount, onMarkedRead })
|
|
117
|
+
useParticipantMarkRead({
|
|
118
|
+
apiRef,
|
|
119
|
+
subject: stableSubject,
|
|
120
|
+
unreadCount: unread.unreadCount,
|
|
121
|
+
messageCount: state.messages.length,
|
|
122
|
+
onMarkedRead: unread.handleMarkedRead,
|
|
123
|
+
})
|
|
113
124
|
|
|
114
125
|
const localPending = useMemo(() => toLocalPending(sendEntries), [sendEntries])
|
|
115
126
|
return { ...state, localPending, confirmsRead, refresh, loadOlder, ...sending }
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { useMemo, type Dispatch, type MutableRefObject } from 'react'
|
|
2
|
+
|
|
3
|
+
import type { ParticipantConversationSummary, ParticipantSubjectRef } from '@adatechnology/conversation-contracts'
|
|
4
|
+
|
|
5
|
+
import type { ConversationChannel } from '../conversationChannel'
|
|
6
|
+
import type { ParticipantThreadProps } from './ParticipantThread'
|
|
7
|
+
import { bindAttachmentUrlResolver } from './bindAttachmentUrlResolver'
|
|
8
|
+
import type { ParticipantConversationsApi, ParticipantPendingMessage } from './participantApi.types'
|
|
9
|
+
import type { ParticipantDrafts } from './participantDrafts'
|
|
10
|
+
import type { ParticipantConversationsLabels } from './participantLabels'
|
|
11
|
+
import type { ParticipantPerspective } from './participantPerspective'
|
|
12
|
+
import { findFailedEntry, hasSendingEntry, type ParticipantSendAction } from './participantSendController'
|
|
13
|
+
import type { ParticipantSendStates, ParticipantSendStatesAction } from './participantSendStates'
|
|
14
|
+
import { useParticipantConversationView } from './useParticipantConversationView'
|
|
15
|
+
import { useParticipantDraft } from './useParticipantDraft'
|
|
16
|
+
import { useParticipantThread, type UseParticipantThreadResult } from './useParticipantThread'
|
|
17
|
+
import { useParticipantThreadScroll } from './useParticipantThreadScroll'
|
|
18
|
+
|
|
19
|
+
export type ParticipantThreadControllerParams = {
|
|
20
|
+
readonly api: ParticipantConversationsApi
|
|
21
|
+
readonly subject: ParticipantSubjectRef
|
|
22
|
+
readonly conversation: ParticipantConversationSummary
|
|
23
|
+
readonly labels: ParticipantConversationsLabels
|
|
24
|
+
/** Holds the immutable drafts of every subject; replaced, never mutated. */
|
|
25
|
+
readonly draftsRef: MutableRefObject<ParticipantDrafts>
|
|
26
|
+
readonly sendStates: ParticipantSendStates
|
|
27
|
+
readonly dispatchSendStates: Dispatch<ParticipantSendStatesAction>
|
|
28
|
+
readonly onMarkedRead: (subject: ParticipantSubjectRef) => void
|
|
29
|
+
readonly channel?: ConversationChannel
|
|
30
|
+
readonly locale?: string
|
|
31
|
+
readonly pendingMessages?: readonly ParticipantPendingMessage[]
|
|
32
|
+
readonly onRetryPending?: (clientMessageId: string) => void
|
|
33
|
+
readonly perspective?: ParticipantPerspective
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Everything a conversation screen needs from the hooks; the caller adds only its own presentation props. */
|
|
37
|
+
export type ParticipantThreadCoreProps = Pick<
|
|
38
|
+
ParticipantThreadProps,
|
|
39
|
+
| 'conversation'
|
|
40
|
+
| 'items'
|
|
41
|
+
| 'hasMore'
|
|
42
|
+
| 'labels'
|
|
43
|
+
| 'resolveAttachmentUrl'
|
|
44
|
+
| 'draft'
|
|
45
|
+
| 'onSend'
|
|
46
|
+
| 'status'
|
|
47
|
+
| 'refresh'
|
|
48
|
+
| 'scroll'
|
|
49
|
+
| 'newMessagesCount'
|
|
50
|
+
| 'isSending'
|
|
51
|
+
| 'channel'
|
|
52
|
+
| 'locale'
|
|
53
|
+
| 'onLoadOlder'
|
|
54
|
+
| 'pendingActions'
|
|
55
|
+
| 'perspective'
|
|
56
|
+
>
|
|
57
|
+
|
|
58
|
+
type BuildPendingActionsParams = {
|
|
59
|
+
readonly thread: UseParticipantThreadResult
|
|
60
|
+
readonly dispatchSend: (action: ParticipantSendAction) => void
|
|
61
|
+
readonly handleEdit: (clientMessageId: string) => void
|
|
62
|
+
readonly onRetryPending: ParticipantThreadControllerParams['onRetryPending']
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function buildPendingActions({ thread, dispatchSend, handleEdit, onRetryPending }: BuildPendingActionsParams): ParticipantThreadCoreProps['pendingActions'] {
|
|
66
|
+
return {
|
|
67
|
+
retryLocal: (clientMessageId) => void thread.retry(clientMessageId),
|
|
68
|
+
discardLocal: (clientMessageId) => dispatchSend({ type: 'discarded', clientMessageId }),
|
|
69
|
+
editLocal: handleEdit,
|
|
70
|
+
...(onRetryPending ? { retryHost: onRetryPending } : {}),
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function useParticipantThreadController(params: ParticipantThreadControllerParams): ParticipantThreadCoreProps {
|
|
75
|
+
const { api, subject, conversation, draftsRef } = params
|
|
76
|
+
const { draft, setText, setFiles, takeForSend, restoreFromEntry } = useParticipantDraft(draftsRef, subject)
|
|
77
|
+
const { thread, items, sendEntries, dispatchSend } = useParticipantConversationView(params)
|
|
78
|
+
const resolveAttachmentUrl = useMemo(() => bindAttachmentUrlResolver(api), [api])
|
|
79
|
+
const { scroll, newMessagesCount } = useParticipantThreadScroll(items, params.perspective)
|
|
80
|
+
|
|
81
|
+
function handleSend(): void {
|
|
82
|
+
const content = takeForSend()
|
|
83
|
+
if (content) void thread.send(content)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function handleEdit(clientMessageId: string): void {
|
|
87
|
+
const entry = findFailedEntry(sendEntries, clientMessageId)
|
|
88
|
+
if (!entry) return
|
|
89
|
+
restoreFromEntry(entry)
|
|
90
|
+
dispatchSend({ type: 'discarded', clientMessageId })
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
conversation,
|
|
95
|
+
items,
|
|
96
|
+
hasMore: thread.hasMore,
|
|
97
|
+
labels: params.labels,
|
|
98
|
+
resolveAttachmentUrl,
|
|
99
|
+
draft: { value: draft.text, onChange: setText, files: draft.files, onFilesChange: setFiles },
|
|
100
|
+
onSend: handleSend,
|
|
101
|
+
status: thread.status,
|
|
102
|
+
refresh: () => void thread.refresh(),
|
|
103
|
+
scroll,
|
|
104
|
+
newMessagesCount,
|
|
105
|
+
isSending: hasSendingEntry(sendEntries),
|
|
106
|
+
channel: params.channel,
|
|
107
|
+
locale: params.locale,
|
|
108
|
+
onLoadOlder: () => void thread.loadOlder(),
|
|
109
|
+
pendingActions: buildPendingActions({ thread, dispatchSend, handleEdit, onRetryPending: params.onRetryPending }),
|
|
110
|
+
perspective: params.perspective,
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -2,6 +2,7 @@ import { useEffect, useRef } from 'react'
|
|
|
2
2
|
|
|
3
3
|
import { collectServerMessageIds, countNewIncomingMessages } from './participantNewMessages'
|
|
4
4
|
import type { ParticipantTimelineItem } from './participantMessages'
|
|
5
|
+
import type { ParticipantPerspective } from './participantPerspective'
|
|
5
6
|
import { useScrollAwayState } from './useScrollAwayState'
|
|
6
7
|
import { useStickToBottom, type ParticipantThreadScroll } from './useStickToBottom'
|
|
7
8
|
|
|
@@ -17,6 +18,7 @@ function keyOf(item: ParticipantTimelineItem | undefined): string | undefined {
|
|
|
17
18
|
|
|
18
19
|
export function useParticipantThreadScroll(
|
|
19
20
|
items: readonly ParticipantTimelineItem[],
|
|
21
|
+
perspective?: ParticipantPerspective,
|
|
20
22
|
): UseParticipantThreadScrollResult {
|
|
21
23
|
const { scroll: base, isNearBottom } = useStickToBottom({
|
|
22
24
|
firstKey: keyOf(items[0]),
|
|
@@ -30,5 +32,5 @@ export function useParticipantThreadScroll(
|
|
|
30
32
|
if (isNearBottom) seenIds.current = collectServerMessageIds(items)
|
|
31
33
|
}, [isNearBottom, items])
|
|
32
34
|
|
|
33
|
-
return { scroll, newMessagesCount: isNearBottom ? 0 : countNewIncomingMessages(seenIds.current, items) }
|
|
35
|
+
return { scroll, newMessagesCount: isNearBottom ? 0 : countNewIncomingMessages(seenIds.current, items, perspective) }
|
|
34
36
|
}
|