@adatechnology/conversations-ui 0.4.2 → 0.5.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 +169 -0
- package/dist/{ConversationSimulatorPanel-DnXElfY8.d.ts → ConversationSimulatorPanel-CrfFjiWh.d.ts} +2 -268
- package/dist/{chunk-PJNR6UMN.js → chunk-CBYEHPX6.js} +1 -63
- package/dist/{chunk-HMCOTZAX.js → chunk-HOKNKXIT.js} +1 -1
- package/dist/{chunk-4P5KMWOI.js → chunk-JXJCPN34.js} +327 -522
- package/dist/chunk-MRVFFJEH.js +64 -0
- package/dist/chunk-RJMSM3YA.js +249 -0
- package/dist/chunk-WSFGNR5R.js +55 -0
- package/dist/flows/index.js +5 -3
- package/dist/index.d.ts +9 -3
- package/dist/index.js +783 -807
- package/dist/participant/index.d.ts +162 -0
- package/dist/participant/index.js +1448 -0
- package/dist/preview/index.d.ts +4 -2
- package/dist/preview/index.js +4 -2
- package/dist/quickReply.types-B4gmZsSr.d.ts +274 -0
- package/dist/styles.css +591 -0
- package/package.json +8 -4
- package/src/ChannelIcon.tsx +2 -0
- package/src/MessageBubble.test.tsx +2 -2
- package/src/MessageText.test.tsx +59 -0
- package/src/MessageText.tsx +20 -12
- package/src/MessageTextCopiedBadge.tsx +7 -0
- package/src/StatusTicks.test.tsx +30 -0
- package/src/StatusTicks.tsx +24 -9
- package/src/buildOutput.test.ts +41 -0
- package/src/channelCapability.test.ts +17 -0
- package/src/conversationChannel.test.ts +30 -1
- package/src/conversationChannel.ts +19 -1
- package/src/participant/ParticipantAttachmentItem.tsx +77 -0
- package/src/participant/ParticipantComposer.test.tsx +97 -0
- package/src/participant/ParticipantComposer.tsx +167 -0
- package/src/participant/ParticipantConversations.test.tsx +90 -0
- package/src/participant/ParticipantConversations.tsx +42 -0
- package/src/participant/ParticipantConversationsScreen.tsx +67 -0
- package/src/participant/ParticipantInbox.test.tsx +147 -0
- package/src/participant/ParticipantInbox.tsx +168 -0
- package/src/participant/ParticipantLoadState.tsx +27 -0
- package/src/participant/ParticipantMessageBubble.test.tsx +165 -0
- package/src/participant/ParticipantMessageBubble.tsx +185 -0
- package/src/participant/ParticipantThread.test.tsx +132 -0
- package/src/participant/ParticipantThread.tsx +174 -0
- package/src/participant/ParticipantThreadScreen.tsx +135 -0
- package/src/participant/bindAttachmentUrlResolver.test.ts +25 -0
- package/src/participant/bindAttachmentUrlResolver.ts +7 -0
- package/src/participant/index.ts +25 -0
- package/src/participant/participant.types.ts +43 -0
- package/src/participant/participantApi.types.ts +59 -0
- package/src/participant/participantAttachmentUrl.test.ts +32 -0
- package/src/participant/participantAttachmentUrl.ts +10 -0
- package/src/participant/participantBubbleActions.test.ts +44 -0
- package/src/participant/participantBubbleActions.ts +40 -0
- package/src/participant/participantDrafts.test.ts +65 -0
- package/src/participant/participantDrafts.ts +50 -0
- package/src/participant/participantFileValidation.test.ts +86 -0
- package/src/participant/participantFileValidation.ts +72 -0
- package/src/participant/participantFixtures.test-helper.ts +41 -0
- package/src/participant/participantGrouping.test.ts +138 -0
- package/src/participant/participantGrouping.ts +77 -0
- package/src/participant/participantInboxState.test.ts +69 -0
- package/src/participant/participantInboxState.ts +74 -0
- package/src/participant/participantLabels.ts +86 -0
- package/src/participant/participantLoadView.test.ts +23 -0
- package/src/participant/participantLoadView.ts +21 -0
- package/src/participant/participantMessages.test.ts +128 -0
- package/src/participant/participantMessages.ts +73 -0
- package/src/participant/participantNewMessages.test.ts +54 -0
- package/src/participant/participantNewMessages.ts +29 -0
- package/src/participant/participantQuickReply.test.ts +23 -0
- package/src/participant/participantQuickReply.ts +6 -0
- package/src/participant/participantScroll.test.ts +69 -0
- package/src/participant/participantScroll.ts +43 -0
- package/src/participant/participantSendController.test.ts +201 -0
- package/src/participant/participantSendController.ts +134 -0
- package/src/participant/participantSendStates.ts +31 -0
- package/src/participant/participantTheme.ts +20 -0
- package/src/participant/participantThreadState.test.ts +75 -0
- package/src/participant/participantThreadState.ts +59 -0
- package/src/participant/shouldMarkRead.test.ts +24 -0
- package/src/participant/shouldMarkRead.ts +16 -0
- package/src/participant/useDocumentVisibility.ts +18 -0
- package/src/participant/useParticipantConversationView.ts +60 -0
- package/src/participant/useParticipantDraft.ts +45 -0
- package/src/participant/useParticipantInbox.ts +57 -0
- package/src/participant/useParticipantMarkRead.ts +26 -0
- package/src/participant/useParticipantRevalidation.ts +36 -0
- package/src/participant/useParticipantSend.ts +84 -0
- package/src/participant/useParticipantThread.ts +116 -0
- package/src/participant/useParticipantThreadScroll.ts +26 -0
- package/src/participant/useStickToBottom.ts +77 -0
- package/src/preview/ConversationSimulatorPanel.tsx +8 -0
- package/src/styles.css +160 -0
- package/src/types.ts +3 -1
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { useCallback, useRef, type MutableRefObject } from 'react'
|
|
2
|
+
|
|
3
|
+
import type { ParticipantMessage, ParticipantSubjectRef } from '@adatechnology/conversation-contracts'
|
|
4
|
+
|
|
5
|
+
import type { ParticipantConversationsApi } from './participantApi.types'
|
|
6
|
+
import type { ParticipantSendContent } from './participantDrafts'
|
|
7
|
+
import {
|
|
8
|
+
createParticipantSendEntry,
|
|
9
|
+
findFailedEntry,
|
|
10
|
+
type ParticipantSendAction,
|
|
11
|
+
type ParticipantSendEntry,
|
|
12
|
+
} from './participantSendController'
|
|
13
|
+
|
|
14
|
+
export type ParticipantSendOutcome = 'sent' | 'queued' | 'failed'
|
|
15
|
+
|
|
16
|
+
export type UseParticipantSendParams = {
|
|
17
|
+
readonly apiRef: MutableRefObject<ParticipantConversationsApi>
|
|
18
|
+
readonly subject: ParticipantSubjectRef
|
|
19
|
+
readonly entries: readonly ParticipantSendEntry[]
|
|
20
|
+
readonly dispatchSend: (action: ParticipantSendAction) => void
|
|
21
|
+
readonly onSentMessage: (message: ParticipantMessage) => void
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type UseParticipantSendResult = {
|
|
25
|
+
readonly send: (content: ParticipantSendContent) => Promise<ParticipantSendOutcome>
|
|
26
|
+
readonly retry: (clientMessageId: string) => Promise<void>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function useParticipantSend(params: UseParticipantSendParams): UseParticipantSendResult {
|
|
30
|
+
const { apiRef, subject, dispatchSend, onSentMessage } = params
|
|
31
|
+
const entriesRef = useRef(params.entries)
|
|
32
|
+
entriesRef.current = params.entries
|
|
33
|
+
|
|
34
|
+
const deliver = useCallback(
|
|
35
|
+
async (entry: ParticipantSendEntry): Promise<ParticipantSendOutcome> => {
|
|
36
|
+
const { clientMessageId } = entry
|
|
37
|
+
try {
|
|
38
|
+
const result = await apiRef.current.sendMessage({
|
|
39
|
+
subject: entry.subject,
|
|
40
|
+
clientMessageId,
|
|
41
|
+
...(entry.text ? { text: entry.text } : {}),
|
|
42
|
+
...(entry.files.length > 0 ? { files: entry.files } : {}),
|
|
43
|
+
})
|
|
44
|
+
if (result.outcome === 'sent') {
|
|
45
|
+
onSentMessage(result.message)
|
|
46
|
+
dispatchSend({ type: 'sent', clientMessageId, message: result.message })
|
|
47
|
+
return 'sent'
|
|
48
|
+
}
|
|
49
|
+
dispatchSend({ type: 'queued', clientMessageId })
|
|
50
|
+
return 'queued'
|
|
51
|
+
} catch {
|
|
52
|
+
dispatchSend({ type: 'failed', clientMessageId })
|
|
53
|
+
return 'failed'
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
[apiRef, dispatchSend, onSentMessage],
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
const send = useCallback(
|
|
60
|
+
async (content: ParticipantSendContent): Promise<ParticipantSendOutcome> => {
|
|
61
|
+
const entry = createParticipantSendEntry({
|
|
62
|
+
clientMessageId: crypto.randomUUID(),
|
|
63
|
+
subject,
|
|
64
|
+
content,
|
|
65
|
+
createdAt: new Date().toISOString(),
|
|
66
|
+
})
|
|
67
|
+
dispatchSend({ type: 'started', entry })
|
|
68
|
+
return deliver(entry)
|
|
69
|
+
},
|
|
70
|
+
[deliver, dispatchSend, subject],
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
const retry = useCallback(
|
|
74
|
+
async (clientMessageId: string): Promise<void> => {
|
|
75
|
+
const entry = findFailedEntry(entriesRef.current, clientMessageId)
|
|
76
|
+
if (!entry) return
|
|
77
|
+
dispatchSend({ type: 'retried', clientMessageId })
|
|
78
|
+
await deliver({ ...entry, state: 'sending' })
|
|
79
|
+
},
|
|
80
|
+
[deliver, dispatchSend],
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
return { send, retry }
|
|
84
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useReducer, useRef } from 'react'
|
|
2
|
+
|
|
3
|
+
import type { ParticipantMessage, ParticipantSubjectRef } from '@adatechnology/conversation-contracts'
|
|
4
|
+
|
|
5
|
+
import { channelCapabilityFor } from '../channelCapability'
|
|
6
|
+
import type { ConversationChannel } from '../conversationChannel'
|
|
7
|
+
import type {
|
|
8
|
+
ParticipantConversationEvent,
|
|
9
|
+
ParticipantConversationsApi,
|
|
10
|
+
ParticipantLocalPendingMessage,
|
|
11
|
+
ParticipantPendingMessage,
|
|
12
|
+
} from './participantApi.types'
|
|
13
|
+
import {
|
|
14
|
+
collectKnownClientMessageIds,
|
|
15
|
+
toLocalPending,
|
|
16
|
+
type ParticipantSendAction,
|
|
17
|
+
type ParticipantSendEntry,
|
|
18
|
+
} from './participantSendController'
|
|
19
|
+
import { INITIAL_PARTICIPANT_THREAD_STATE, participantThreadReducer, type ParticipantThreadState } from './participantThreadState'
|
|
20
|
+
import { useParticipantMarkRead } from './useParticipantMarkRead'
|
|
21
|
+
import { useParticipantRevalidation } from './useParticipantRevalidation'
|
|
22
|
+
import { useParticipantSend, type UseParticipantSendResult } from './useParticipantSend'
|
|
23
|
+
|
|
24
|
+
const THREAD_PAGE_SIZE = 30
|
|
25
|
+
|
|
26
|
+
export type UseParticipantThreadParams = {
|
|
27
|
+
/** Keep it stable: a new identity re-subscribes to events (it never resets the conversation). */
|
|
28
|
+
readonly api: ParticipantConversationsApi
|
|
29
|
+
readonly subject: ParticipantSubjectRef
|
|
30
|
+
readonly channel?: ConversationChannel
|
|
31
|
+
/** Unread count of this subject as the inbox knows it; drives markRead. */
|
|
32
|
+
readonly unreadCount?: number
|
|
33
|
+
readonly onMarkedRead?: (subject: ParticipantSubjectRef) => void
|
|
34
|
+
/** Sends of this subject, owned above the conversation so they survive leaving it. */
|
|
35
|
+
readonly sendEntries: readonly ParticipantSendEntry[]
|
|
36
|
+
readonly dispatchSend: (action: ParticipantSendAction) => void
|
|
37
|
+
readonly hostPending?: readonly ParticipantPendingMessage[]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type UseParticipantThreadResult = ParticipantThreadState &
|
|
41
|
+
UseParticipantSendResult & {
|
|
42
|
+
readonly confirmsRead: boolean
|
|
43
|
+
readonly localPending: readonly ParticipantLocalPendingMessage[]
|
|
44
|
+
readonly refresh: () => Promise<void>
|
|
45
|
+
readonly loadOlder: () => Promise<void>
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function describeError(error: unknown): string {
|
|
49
|
+
return error instanceof Error ? error.message : 'unknown error'
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function isForSubject(event: ParticipantConversationEvent | undefined, subject: ParticipantSubjectRef): boolean {
|
|
53
|
+
if (!event || event.type === 'inbox-changed') return true
|
|
54
|
+
return event.subject.subjectType === subject.subjectType && event.subject.subjectId === subject.subjectId
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function useParticipantThread(params: UseParticipantThreadParams): UseParticipantThreadResult {
|
|
58
|
+
const { api, subject, channel, unreadCount = 0, onMarkedRead, sendEntries, dispatchSend, hostPending = [] } = params
|
|
59
|
+
const { subjectType, subjectId } = subject
|
|
60
|
+
const stableSubject = useMemo<ParticipantSubjectRef>(() => ({ subjectType, subjectId }), [subjectType, subjectId])
|
|
61
|
+
const [state, dispatch] = useReducer(participantThreadReducer, INITIAL_PARTICIPANT_THREAD_STATE)
|
|
62
|
+
const apiRef = useRef(api)
|
|
63
|
+
apiRef.current = api
|
|
64
|
+
const activeSubjectKey = useRef('')
|
|
65
|
+
const confirmsRead = channelCapabilityFor(channel).confirmsRead
|
|
66
|
+
|
|
67
|
+
const refresh = useCallback(async (): Promise<void> => {
|
|
68
|
+
const requestKey = `${subjectType}:${subjectId}`
|
|
69
|
+
dispatch({ type: 'started' })
|
|
70
|
+
try {
|
|
71
|
+
const messages = await apiRef.current.fetchMessages(stableSubject, { limit: THREAD_PAGE_SIZE })
|
|
72
|
+
if (activeSubjectKey.current === requestKey) {
|
|
73
|
+
dispatch({ type: 'loaded', messages, hasMore: messages.length >= THREAD_PAGE_SIZE })
|
|
74
|
+
}
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if (activeSubjectKey.current === requestKey) dispatch({ type: 'failed', error: describeError(error) })
|
|
77
|
+
}
|
|
78
|
+
}, [stableSubject, subjectType, subjectId])
|
|
79
|
+
|
|
80
|
+
const oldestMessageId = state.messages[0]?.id
|
|
81
|
+
const loadOlder = useCallback(async (): Promise<void> => {
|
|
82
|
+
if (!oldestMessageId) return
|
|
83
|
+
const requestKey = `${subjectType}:${subjectId}`
|
|
84
|
+
try {
|
|
85
|
+
const messages = await apiRef.current.fetchMessages(stableSubject, { before: oldestMessageId, limit: THREAD_PAGE_SIZE })
|
|
86
|
+
if (activeSubjectKey.current === requestKey) {
|
|
87
|
+
dispatch({ type: 'olderLoaded', messages, hasMore: messages.length >= THREAD_PAGE_SIZE })
|
|
88
|
+
}
|
|
89
|
+
} catch (error) {
|
|
90
|
+
if (activeSubjectKey.current === requestKey) dispatch({ type: 'failed', error: describeError(error) })
|
|
91
|
+
}
|
|
92
|
+
}, [stableSubject, subjectType, subjectId, oldestMessageId])
|
|
93
|
+
|
|
94
|
+
const handleSentMessage = useCallback((message: ParticipantMessage) => dispatch({ type: 'sendConfirmed', message }), [])
|
|
95
|
+
const sending = useParticipantSend({ apiRef, subject: stableSubject, entries: sendEntries, dispatchSend, onSentMessage: handleSentMessage })
|
|
96
|
+
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
activeSubjectKey.current = `${subjectType}:${subjectId}`
|
|
99
|
+
dispatch({ type: 'reset' })
|
|
100
|
+
void refresh()
|
|
101
|
+
}, [refresh, subjectType, subjectId])
|
|
102
|
+
|
|
103
|
+
useParticipantRevalidation(api, (event) => {
|
|
104
|
+
if (isForSubject(event, stableSubject)) void refresh()
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
if (sendEntries.length === 0) return
|
|
109
|
+
dispatchSend({ type: 'reflected', knownClientMessageIds: collectKnownClientMessageIds({ messages: state.messages, hostPending }) })
|
|
110
|
+
}, [sendEntries, state.messages, hostPending, dispatchSend])
|
|
111
|
+
|
|
112
|
+
useParticipantMarkRead({ apiRef, subject: stableSubject, unreadCount, messageCount: state.messages.length, onMarkedRead })
|
|
113
|
+
|
|
114
|
+
const localPending = useMemo(() => toLocalPending(sendEntries), [sendEntries])
|
|
115
|
+
return { ...state, localPending, confirmsRead, refresh, loadOlder, ...sending }
|
|
116
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react'
|
|
2
|
+
|
|
3
|
+
import { collectServerMessageIds, countNewIncomingMessages } from './participantNewMessages'
|
|
4
|
+
import type { ParticipantTimelineItem } from './participantMessages'
|
|
5
|
+
import { useStickToBottom, type ParticipantThreadScroll } from './useStickToBottom'
|
|
6
|
+
|
|
7
|
+
export type UseParticipantThreadScrollResult = {
|
|
8
|
+
readonly scroll: ParticipantThreadScroll
|
|
9
|
+
readonly newMessagesCount: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function keyOf(item: ParticipantTimelineItem | undefined): string | undefined {
|
|
13
|
+
if (!item) return undefined
|
|
14
|
+
return item.kind === 'server' ? item.message.id : `pending:${item.pending.clientMessageId}`
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function useParticipantThreadScroll(items: readonly ParticipantTimelineItem[]): UseParticipantThreadScrollResult {
|
|
18
|
+
const { scroll, isNearBottom } = useStickToBottom({ firstKey: keyOf(items[0]), lastKey: keyOf(items[items.length - 1]) })
|
|
19
|
+
const seenIds = useRef<ReadonlySet<string>>(new Set())
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (isNearBottom) seenIds.current = collectServerMessageIds(items)
|
|
23
|
+
}, [isNearBottom, items])
|
|
24
|
+
|
|
25
|
+
return { scroll, newMessagesCount: isNearBottom ? 0 : countNewIncomingMessages(seenIds.current, items) }
|
|
26
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { useCallback, useLayoutEffect, useRef, useState, type RefObject, type UIEvent } from 'react'
|
|
2
|
+
|
|
3
|
+
import { NEAR_BOTTOM_THRESHOLD_PX, resolveScrollAction, scrollAnchorAfterPrepend, shouldStickToBottom } from './participantScroll'
|
|
4
|
+
|
|
5
|
+
export type UseStickToBottomParams = {
|
|
6
|
+
readonly firstKey: string | undefined
|
|
7
|
+
readonly lastKey: string | undefined
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type ParticipantThreadScroll = {
|
|
11
|
+
readonly ref: RefObject<HTMLDivElement | null>
|
|
12
|
+
readonly onScroll: (event: UIEvent<HTMLDivElement>) => void
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type UseStickToBottomResult = {
|
|
16
|
+
readonly scroll: ParticipantThreadScroll
|
|
17
|
+
readonly isNearBottom: boolean
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type Snapshot = {
|
|
21
|
+
hasPositioned: boolean
|
|
22
|
+
firstKey: string | undefined
|
|
23
|
+
lastKey: string | undefined
|
|
24
|
+
scrollTop: number
|
|
25
|
+
scrollHeight: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function useStickToBottom({ firstKey, lastKey }: UseStickToBottomParams): UseStickToBottomResult {
|
|
29
|
+
const ref = useRef<HTMLDivElement | null>(null)
|
|
30
|
+
const snapshot = useRef<Snapshot>({ hasPositioned: false, firstKey: undefined, lastKey: undefined, scrollTop: 0, scrollHeight: 0 })
|
|
31
|
+
const [isNearBottom, setIsNearBottom] = useState(true)
|
|
32
|
+
const isNearBottomRef = useRef(true)
|
|
33
|
+
|
|
34
|
+
const onScroll = useCallback((event: UIEvent<HTMLDivElement>): void => {
|
|
35
|
+
const { scrollTop, clientHeight, scrollHeight } = event.currentTarget
|
|
36
|
+
const isNear = shouldStickToBottom({ scrollTop, clientHeight, scrollHeight, threshold: NEAR_BOTTOM_THRESHOLD_PX })
|
|
37
|
+
snapshot.current.scrollTop = scrollTop
|
|
38
|
+
snapshot.current.scrollHeight = scrollHeight
|
|
39
|
+
isNearBottomRef.current = isNear
|
|
40
|
+
setIsNearBottom(isNear)
|
|
41
|
+
}, [])
|
|
42
|
+
|
|
43
|
+
useLayoutEffect(() => {
|
|
44
|
+
const element = ref.current
|
|
45
|
+
const previous = snapshot.current
|
|
46
|
+
if (!element) return
|
|
47
|
+
const action = resolveScrollAction({
|
|
48
|
+
hasPositioned: previous.hasPositioned,
|
|
49
|
+
previousFirstKey: previous.firstKey,
|
|
50
|
+
previousLastKey: previous.lastKey,
|
|
51
|
+
firstKey,
|
|
52
|
+
lastKey,
|
|
53
|
+
wasNearBottom: isNearBottomRef.current,
|
|
54
|
+
})
|
|
55
|
+
if (action === 'bottom') {
|
|
56
|
+
element.scrollTop = element.scrollHeight
|
|
57
|
+
isNearBottomRef.current = true
|
|
58
|
+
setIsNearBottom(true)
|
|
59
|
+
}
|
|
60
|
+
if (action === 'anchor') {
|
|
61
|
+
element.scrollTop = scrollAnchorAfterPrepend({
|
|
62
|
+
previousScrollTop: previous.scrollTop,
|
|
63
|
+
previousScrollHeight: previous.scrollHeight,
|
|
64
|
+
scrollHeight: element.scrollHeight,
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
snapshot.current = {
|
|
68
|
+
hasPositioned: previous.hasPositioned || action === 'bottom',
|
|
69
|
+
firstKey,
|
|
70
|
+
lastKey,
|
|
71
|
+
scrollTop: element.scrollTop,
|
|
72
|
+
scrollHeight: element.scrollHeight,
|
|
73
|
+
}
|
|
74
|
+
}, [firstKey, lastKey])
|
|
75
|
+
|
|
76
|
+
return { scroll: { ref, onScroll }, isNearBottom }
|
|
77
|
+
}
|
|
@@ -62,6 +62,14 @@ const SIMULATOR_PANEL_CHANNEL_WORDING: Readonly<Record<ConversationChannel, Chan
|
|
|
62
62
|
destinationHint: DEFAULT_CONVERSATION_SIMULATOR_PANEL_LABELS.destinationHint,
|
|
63
63
|
placeholder: DEFAULT_CONVERSATION_SIMULATOR_PANEL_LABELS.placeholder,
|
|
64
64
|
},
|
|
65
|
+
[CONVERSATION_CHANNEL.APP]: {
|
|
66
|
+
destinationHint: 'entrega no app do contratante',
|
|
67
|
+
placeholder: 'Escreva como quem usa o app…',
|
|
68
|
+
},
|
|
69
|
+
[CONVERSATION_CHANNEL.PORTAL]: {
|
|
70
|
+
destinationHint: 'entrega no portal do contratante',
|
|
71
|
+
placeholder: 'Escreva como quem usa o portal…',
|
|
72
|
+
},
|
|
65
73
|
[CONVERSATION_CHANNEL.WEBCHAT]: {
|
|
66
74
|
destinationHint: 'entrega na API do chat do site',
|
|
67
75
|
placeholder: 'Escreva como o visitante…',
|
package/src/styles.css
CHANGED
|
@@ -1119,3 +1119,163 @@
|
|
|
1119
1119
|
border-top-color: rgb(51 65 85);
|
|
1120
1120
|
background: rgb(17 24 39);
|
|
1121
1121
|
}
|
|
1122
|
+
|
|
1123
|
+
.cv-status-ticks { display: flex; align-items: center; line-height: 1; cursor: help; }
|
|
1124
|
+
.cv-status-ticks--queued,
|
|
1125
|
+
.cv-status-ticks--sent,
|
|
1126
|
+
.cv-status-ticks--delivered { color: rgb(0 0 0 / 40%); }
|
|
1127
|
+
.dark .cv-status-ticks--queued,
|
|
1128
|
+
.dark .cv-status-ticks--sent,
|
|
1129
|
+
.dark .cv-status-ticks--delivered { color: rgb(255 255 255 / 40%); }
|
|
1130
|
+
.cv-status-ticks--read { color: rgb(14 165 233); }
|
|
1131
|
+
.cv-status-ticks--failed { color: rgb(239 68 68); }
|
|
1132
|
+
|
|
1133
|
+
.cv-message-text { position: relative; font-size: 14.2px; line-height: 19px; white-space: pre-wrap; overflow-wrap: break-word; word-break: break-word; }
|
|
1134
|
+
.cv-message-text--copyable { user-select: all; cursor: pointer; }
|
|
1135
|
+
.cv-message-text strong { font-weight: 700; }
|
|
1136
|
+
.cv-message-text em { font-style: italic; }
|
|
1137
|
+
.cv-message-text del { text-decoration-line: line-through; }
|
|
1138
|
+
.cv-message-text__copied { position: absolute; top: 0; right: 0; transform: translateY(-100%); background: #3b4a54; color: #fff; font-size: 11px; padding: 0.125rem 0.375rem; border-radius: 0.25rem; box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); }
|
|
1139
|
+
|
|
1140
|
+
/* Participant view (.cv-p-*). Public theming API: override --cv-p-surface, --cv-p-surface-raised,
|
|
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 on .cv-p or any ancestor. */
|
|
1143
|
+
.cv-p {
|
|
1144
|
+
--cv-p-i-surface: var(--cv-p-surface, #ffffff);
|
|
1145
|
+
--cv-p-i-surface-raised: var(--cv-p-surface-raised, #f3f4f6);
|
|
1146
|
+
--cv-p-i-text: var(--cv-p-text, #111827);
|
|
1147
|
+
--cv-p-i-text-muted: var(--cv-p-text-muted, #5b6573);
|
|
1148
|
+
--cv-p-i-border: var(--cv-p-border, #d5d9df);
|
|
1149
|
+
--cv-p-i-accent: var(--cv-p-accent, #a85a1c);
|
|
1150
|
+
--cv-p-i-accent-contrast: var(--cv-p-accent-contrast, #ffffff);
|
|
1151
|
+
--cv-p-i-highlight: var(--cv-p-highlight, #fbefe2);
|
|
1152
|
+
--cv-p-i-danger: var(--cv-p-danger, #c62828);
|
|
1153
|
+
--cv-p-i-radius: var(--cv-p-radius, 0);
|
|
1154
|
+
--cv-p-i-touch: 2.75rem;
|
|
1155
|
+
display: flex;
|
|
1156
|
+
flex-direction: column;
|
|
1157
|
+
min-height: 0;
|
|
1158
|
+
height: 100%;
|
|
1159
|
+
background: var(--cv-p-i-surface);
|
|
1160
|
+
color: var(--cv-p-i-text);
|
|
1161
|
+
font-family: 'Avenir Next', Avenir, 'Segoe UI', sans-serif;
|
|
1162
|
+
line-height: 1.4;
|
|
1163
|
+
box-sizing: border-box;
|
|
1164
|
+
}
|
|
1165
|
+
.dark .cv-p {
|
|
1166
|
+
--cv-p-i-surface: var(--cv-p-surface, #10222c);
|
|
1167
|
+
--cv-p-i-surface-raised: var(--cv-p-surface-raised, #1c2b33);
|
|
1168
|
+
--cv-p-i-text: var(--cv-p-text, #f0f2ee);
|
|
1169
|
+
--cv-p-i-text-muted: var(--cv-p-text-muted, #8fa3ad);
|
|
1170
|
+
--cv-p-i-border: var(--cv-p-border, rgb(143 163 173 / 26%));
|
|
1171
|
+
--cv-p-i-accent: var(--cv-p-accent, #d58a47);
|
|
1172
|
+
--cv-p-i-accent-contrast: var(--cv-p-accent-contrast, #10222c);
|
|
1173
|
+
--cv-p-i-highlight: var(--cv-p-highlight, rgb(213 138 71 / 10%));
|
|
1174
|
+
--cv-p-i-danger: var(--cv-p-danger, #ff6b63);
|
|
1175
|
+
}
|
|
1176
|
+
.cv-p *, .cv-p *::before, .cv-p *::after { box-sizing: border-box; }
|
|
1177
|
+
.cv-p :focus-visible { outline: 2px solid var(--cv-p-i-accent); outline-offset: 2px; }
|
|
1178
|
+
.cv-p-sr-only {
|
|
1179
|
+
position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
|
|
1180
|
+
overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
.cv-p-button, .cv-p-chip, .cv-p-row, .cv-p-thread__older, .cv-p-bubble__retry, .cv-p-bubble__action {
|
|
1184
|
+
font: inherit; color: inherit; cursor: pointer;
|
|
1185
|
+
}
|
|
1186
|
+
.cv-p-button {
|
|
1187
|
+
min-height: var(--cv-p-i-touch); min-width: var(--cv-p-i-touch); padding: 0 0.75rem;
|
|
1188
|
+
background: transparent; border: 1px solid var(--cv-p-i-border); border-radius: var(--cv-p-i-radius);
|
|
1189
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
1190
|
+
}
|
|
1191
|
+
.cv-p-button:disabled { opacity: 0.45; cursor: not-allowed; }
|
|
1192
|
+
.cv-p-button--primary { background: var(--cv-p-i-accent); border-color: var(--cv-p-i-accent); color: var(--cv-p-i-accent-contrast); font-weight: 600; }
|
|
1193
|
+
.cv-p-chip {
|
|
1194
|
+
flex: none; min-height: 2.75rem; padding: 0 0.875rem; display: inline-flex; align-items: center; gap: 0.4rem;
|
|
1195
|
+
background: transparent; border: 1px solid var(--cv-p-i-border); border-radius: var(--cv-p-i-radius); color: var(--cv-p-i-text-muted);
|
|
1196
|
+
font-size: 0.85rem;
|
|
1197
|
+
}
|
|
1198
|
+
.cv-p-chip[aria-pressed='true'] { background: var(--cv-p-i-text); border-color: var(--cv-p-i-text); color: var(--cv-p-i-surface); }
|
|
1199
|
+
.cv-p-chip__count {
|
|
1200
|
+
font: 0.7rem 'SFMono-Regular', Consolas, monospace; background: var(--cv-p-i-accent);
|
|
1201
|
+
color: var(--cv-p-i-accent-contrast); padding: 0 0.35rem;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
.cv-p-inbox { overflow-y: auto; }
|
|
1205
|
+
.cv-p-inbox__title { margin: 0; padding: 0.75rem 1rem; font-size: 1rem; border-bottom: 1px solid var(--cv-p-i-border); }
|
|
1206
|
+
.cv-p-filters { display: flex; gap: 0.5rem; padding: 0.625rem 1rem; overflow-x: auto; border-bottom: 1px solid var(--cv-p-i-border); }
|
|
1207
|
+
.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); }
|
|
1208
|
+
.cv-p-error__message { margin: 0; }
|
|
1209
|
+
.cv-p-loading { display: flex; flex-direction: column; gap: 0.5rem; padding: 1rem; }
|
|
1210
|
+
.cv-p-skeleton { display: block; height: 3rem; border-radius: var(--cv-p-i-radius); background: var(--cv-p-i-surface-raised); }
|
|
1211
|
+
.cv-p-inbox__more { display: block; margin: 0.75rem auto; padding: 0 1rem; }
|
|
1212
|
+
.cv-p-empty { padding: 2rem 1rem; text-align: center; color: var(--cv-p-i-text-muted); }
|
|
1213
|
+
.cv-p-section__heading {
|
|
1214
|
+
display: flex; justify-content: space-between; margin: 0; padding: 0.875rem 1rem 0.375rem;
|
|
1215
|
+
font: 0.68rem 'SFMono-Regular', Consolas, monospace; letter-spacing: 0.1em; text-transform: uppercase;
|
|
1216
|
+
color: var(--cv-p-i-text-muted); list-style: none;
|
|
1217
|
+
}
|
|
1218
|
+
.cv-p-section--awaiting .cv-p-section__heading { color: var(--cv-p-i-accent); }
|
|
1219
|
+
.cv-p-section--closed > summary { cursor: pointer; min-height: var(--cv-p-i-touch); align-items: center; }
|
|
1220
|
+
.cv-p-section--closed > summary::-webkit-details-marker { display: none; }
|
|
1221
|
+
|
|
1222
|
+
.cv-p-row {
|
|
1223
|
+
width: 100%; min-height: var(--cv-p-i-touch); display: grid; grid-template-columns: 2.25rem 1fr auto; gap: 0.625rem;
|
|
1224
|
+
padding: 0.75rem 1rem; text-align: left; background: transparent; border: 0;
|
|
1225
|
+
border-bottom: 1px solid var(--cv-p-i-border); border-left: 3px solid transparent;
|
|
1226
|
+
}
|
|
1227
|
+
.cv-p-row--awaiting { border-left-color: var(--cv-p-i-accent); background: var(--cv-p-i-highlight); }
|
|
1228
|
+
.cv-p-row__icon {
|
|
1229
|
+
width: 2.25rem; height: 2.25rem; display: grid; place-items: center; border: 1px solid var(--cv-p-i-border);
|
|
1230
|
+
border-radius: var(--cv-p-i-radius); font: 0.7rem 'SFMono-Regular', Consolas, monospace; text-transform: uppercase; color: var(--cv-p-i-accent);
|
|
1231
|
+
}
|
|
1232
|
+
.cv-p-row__body { display: flex; flex-direction: column; min-width: 0; }
|
|
1233
|
+
.cv-p-row__kind { font: 0.64rem 'SFMono-Regular', Consolas, monospace; letter-spacing: 0.08em; text-transform: uppercase; color: var(--cv-p-i-text-muted); }
|
|
1234
|
+
.cv-p-row__title { font-weight: 600; font-size: 0.92rem; }
|
|
1235
|
+
.cv-p-row__preview { color: var(--cv-p-i-text-muted); font-size: 0.82rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
1236
|
+
.cv-p-row__meta { display: flex; flex-direction: column; align-items: flex-end; gap: 0.25rem; font: 0.7rem 'SFMono-Regular', Consolas, monospace; color: var(--cv-p-i-text-muted); }
|
|
1237
|
+
.cv-p-row__unread { min-width: 1.25rem; padding: 0 0.3rem; text-align: center; font-weight: 700; background: var(--cv-p-i-accent); color: var(--cv-p-i-accent-contrast); }
|
|
1238
|
+
|
|
1239
|
+
.cv-p-thread__header { display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem 1rem; border-bottom: 1px solid var(--cv-p-i-border); }
|
|
1240
|
+
.cv-p-thread__title { flex: 1; margin: 0; font-size: 1rem; min-width: 0; }
|
|
1241
|
+
.cv-p-thread__title-button { min-height: var(--cv-p-i-touch); padding: 0; background: none; border: 0; color: inherit; font: inherit; font-weight: 600; text-align: left; text-decoration: underline; cursor: pointer; }
|
|
1242
|
+
.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; }
|
|
1243
|
+
.cv-p-thread__scroll { flex: 1; min-height: 0; overflow-y: auto; overflow-anchor: none; padding: 0.5rem 1rem 1rem; }
|
|
1244
|
+
.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); }
|
|
1245
|
+
.cv-p-thread__messages { display: flex; flex-direction: column; gap: 0.5rem; }
|
|
1246
|
+
.cv-p-thread__live { padding: 0 1rem; font-size: 0.75rem; color: var(--cv-p-i-accent); }
|
|
1247
|
+
.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; }
|
|
1248
|
+
.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); }
|
|
1249
|
+
.cv-p-day { align-self: center; margin: 0.5rem 0; font: 0.65rem 'SFMono-Regular', Consolas, monospace; text-transform: uppercase; color: var(--cv-p-i-text-muted); }
|
|
1250
|
+
|
|
1251
|
+
.cv-p-bubble { max-width: 82%; align-self: flex-start; padding: 0.5rem 0.7rem; font-size: 0.88rem; background: var(--cv-p-i-surface-raised); border: 1px solid var(--cv-p-i-border); border-radius: var(--cv-p-i-radius); }
|
|
1252
|
+
.cv-p-bubble--mine { align-self: flex-end; background: var(--cv-p-i-highlight); border-color: var(--cv-p-i-accent); }
|
|
1253
|
+
.cv-p-bubble__author { display: block; margin-bottom: 0.15rem; font: 0.65rem 'SFMono-Regular', Consolas, monospace; text-transform: uppercase; color: var(--cv-p-i-accent); }
|
|
1254
|
+
.cv-p-bubble__meta { display: flex; justify-content: flex-end; align-items: center; gap: 0.35rem; margin-top: 0.25rem; font: 0.65rem 'SFMono-Regular', Consolas, monospace; color: var(--cv-p-i-text-muted); }
|
|
1255
|
+
.cv-p-bubble__status { display: inline-flex; align-items: center; gap: 0.25rem; }
|
|
1256
|
+
.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; }
|
|
1257
|
+
.cv-p-bubble__actions { display: inline-flex; gap: 0.5rem; margin-top: 0.25rem; }
|
|
1258
|
+
.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; }
|
|
1259
|
+
.cv-p-attachment { display: flex; flex-direction: column; gap: 0.25rem; margin-top: 0.35rem; color: inherit; }
|
|
1260
|
+
.cv-p-attachment--document { flex-direction: row; justify-content: space-between; min-height: var(--cv-p-i-touch); align-items: center; text-decoration: underline; }
|
|
1261
|
+
.cv-p-attachment__image { display: block; max-width: 100%; max-height: 16rem; object-fit: contain; }
|
|
1262
|
+
.cv-p-attachment__audio { width: 100%; min-height: var(--cv-p-i-touch); }
|
|
1263
|
+
.cv-p-attachment__name, .cv-p-attachment__size { font-size: 0.75rem; overflow-wrap: anywhere; }
|
|
1264
|
+
|
|
1265
|
+
.cv-p-composer { display: flex; flex-direction: column; gap: 0.5rem; padding: 0.5rem 1rem 1rem; border-top: 1px solid var(--cv-p-i-border); }
|
|
1266
|
+
.cv-p-composer__row { display: flex; align-items: flex-end; gap: 0.5rem; }
|
|
1267
|
+
.cv-p-composer__input { flex: 1; min-height: var(--cv-p-i-touch); max-height: 8rem; resize: none; padding: 0.7rem; font: inherit; font-size: 0.9rem; color: inherit; background: var(--cv-p-i-surface); border: 1px solid var(--cv-p-i-border); border-radius: var(--cv-p-i-radius); }
|
|
1268
|
+
.cv-p-composer__error { margin: 0; font-size: 0.8rem; color: var(--cv-p-i-danger); }
|
|
1269
|
+
.cv-p-quick { display: flex; gap: 0.5rem; overflow-x: auto; }
|
|
1270
|
+
.cv-p-files { display: flex; flex-direction: column; gap: 0.25rem; margin: 0; padding: 0; list-style: none; }
|
|
1271
|
+
.cv-p-files__item { display: flex; align-items: center; gap: 0.5rem; font-size: 0.8rem; }
|
|
1272
|
+
.cv-p-files__name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1273
|
+
.cv-p-files__size { color: var(--cv-p-i-text-muted); }
|
|
1274
|
+
.cv-p-files__remove { min-width: var(--cv-p-i-touch); min-height: var(--cv-p-i-touch); background: transparent; border: 0; color: inherit; font: inherit; cursor: pointer; }
|
|
1275
|
+
|
|
1276
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
1277
|
+
.cv-p-row, .cv-p-button, .cv-p-chip { transition: background-color 0.12s, border-color 0.12s; }
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
.cv-p-root { display: flex; flex-direction: column; min-height: 0; height: 100%; }
|
|
1281
|
+
.cv-p-root__screen { display: flex; flex-direction: column; flex: 1; min-height: 0; }
|
package/src/types.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { MessageDeliveryStatus } from '@adatechnology/conversation-contracts'
|
|
2
|
+
|
|
1
3
|
export interface ConversationsUIConfig {
|
|
2
4
|
apiBaseUrl: string
|
|
3
5
|
theme?: ConversationsTheme
|
|
@@ -72,7 +74,7 @@ export interface MessagePayload {
|
|
|
72
74
|
direction: 'inbound' | 'outbound'
|
|
73
75
|
sender: 'bot' | 'customer' | 'agent'
|
|
74
76
|
timestamp: string
|
|
75
|
-
status?:
|
|
77
|
+
status?: MessageDeliveryStatus
|
|
76
78
|
readAt?: string
|
|
77
79
|
agentName?: string | null
|
|
78
80
|
templateName?: string
|