@adatechnology/conversations-ui 0.6.1 → 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 +221 -19
- package/dist/{ConversationSimulatorPanel-CrfFjiWh.d.ts → ConversationSimulatorPanel-q-wVgBCN.d.ts} +1 -1
- package/dist/{chunk-HOKNKXIT.js → chunk-HMCOTZAX.js} +1 -1
- package/dist/{chunk-FAQLC3TX.js → chunk-L3OFAQPS.js} +2 -4
- package/dist/{chunk-CBYEHPX6.js → chunk-PJNR6UMN.js} +63 -1
- package/dist/flows/index.js +3 -5
- package/dist/index.d.ts +4 -4
- package/dist/index.js +834 -764
- package/dist/participant/index.d.ts +62 -3
- package/dist/participant/index.js +1444 -394
- package/dist/preview/index.d.ts +3 -3
- package/dist/preview/index.js +2 -3
- package/dist/{quickReply.types-B4gmZsSr.d.ts → quickReply.types-DzWH3NYV.d.ts} +1 -1
- package/dist/styles.css +629 -20
- package/package.json +3 -3
- package/src/participant/ConversationThread.test.tsx +165 -0
- package/src/participant/ConversationThread.tsx +117 -0
- package/src/participant/ParticipantAuthorAvatar.test.tsx +30 -0
- package/src/participant/ParticipantAuthorAvatar.tsx +24 -0
- package/src/participant/ParticipantBackButton.tsx +14 -0
- package/src/participant/ParticipantBubbleStatus.tsx +67 -0
- package/src/participant/ParticipantChannelBadges.tsx +13 -4
- package/src/participant/ParticipantComposer.tsx +8 -40
- package/src/participant/ParticipantComposerControls.tsx +83 -0
- package/src/participant/ParticipantConversationAvatar.tsx +13 -0
- package/src/participant/ParticipantConversations.tsx +7 -0
- package/src/participant/ParticipantConversationsOptions.test.tsx +87 -0
- package/src/participant/ParticipantCopyAnnouncer.tsx +48 -0
- package/src/participant/ParticipantCopyableToken.tsx +51 -0
- package/src/participant/ParticipantDayDivider.tsx +18 -0
- package/src/participant/ParticipantInbox.tsx +3 -1
- package/src/participant/ParticipantInboxRow.tsx +6 -4
- package/src/participant/ParticipantMessageBubble.test.tsx +52 -5
- package/src/participant/ParticipantMessageBubble.tsx +32 -84
- package/src/participant/ParticipantMessageNodes.tsx +54 -0
- package/src/participant/ParticipantMessageText.test.tsx +106 -0
- package/src/participant/ParticipantMessageText.tsx +52 -0
- package/src/participant/ParticipantProtocolBadge.tsx +15 -7
- package/src/participant/ParticipantProtocolView.test.tsx +4 -6
- package/src/participant/ParticipantScrollToLatest.test.tsx +153 -0
- package/src/participant/ParticipantScrollToLatest.tsx +40 -0
- package/src/participant/ParticipantThread.test.tsx +37 -0
- package/src/participant/ParticipantThread.tsx +54 -95
- package/src/participant/ParticipantThreadAvatarSequence.test.tsx +65 -0
- package/src/participant/ParticipantThreadHeader.test.tsx +181 -0
- package/src/participant/ParticipantThreadHeader.tsx +83 -0
- package/src/participant/ParticipantThreadScreen.tsx +30 -56
- package/src/participant/ParticipantTimeline.tsx +106 -0
- 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 +4 -0
- package/src/participant/participantAutoGrow.test.ts +43 -0
- package/src/participant/participantAutoGrow.ts +12 -0
- package/src/participant/participantAvatar.test.ts +74 -0
- package/src/participant/participantAvatar.ts +48 -0
- package/src/participant/participantBubbleTail.test.tsx +107 -0
- package/src/participant/participantChatDesign.test.tsx +198 -0
- package/src/participant/participantContrast.test-helper.ts +116 -0
- package/src/participant/participantCopyAnnouncement.test.ts +52 -0
- package/src/participant/participantCopyAnnouncement.ts +37 -0
- package/src/participant/participantCss.test-helper.ts +34 -0
- package/src/participant/participantDay.test.ts +22 -0
- package/src/participant/participantDay.ts +8 -0
- package/src/participant/participantDocuments.ts +45 -0
- package/src/participant/participantGolden.test.tsx +46 -0
- package/src/participant/participantGoldenScenarios.test-helper.tsx +57 -0
- package/src/participant/participantHeaderA11y.test.tsx +85 -0
- package/src/participant/participantHeaderDesign.test.tsx +111 -0
- package/src/participant/participantLabels.ts +11 -0
- package/src/participant/participantMessageAtoms.ts +53 -0
- package/src/participant/participantMessageCopyables.test.ts +141 -0
- package/src/participant/participantMessageCopyables.ts +76 -0
- package/src/participant/participantMessageFormat.test-helper.ts +21 -0
- package/src/participant/participantMessageFormat.test.ts +177 -0
- package/src/participant/participantMessageFormat.ts +135 -0
- package/src/participant/participantMessageFormat.types.ts +22 -0
- package/src/participant/participantMessageInline.test.ts +82 -0
- package/src/participant/participantMessageInline.ts +99 -0
- package/src/participant/participantMessageLinks.test.ts +100 -0
- package/src/participant/participantMessageLinks.ts +54 -0
- package/src/participant/participantMessageTextStyle.test.ts +137 -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/participantScrollFocus.test.ts +22 -0
- package/src/participant/participantScrollFocus.ts +10 -0
- package/src/participant/participantScrollView.test.ts +74 -0
- package/src/participant/participantScrollView.ts +38 -0
- package/src/participant/participantScrollbars.test.ts +156 -0
- package/src/participant/participantSubjectIcon.test.tsx +27 -0
- package/src/participant/participantSubjectIcon.ts +26 -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/useParticipantCopyFeedback.ts +33 -0
- package/src/participant/useParticipantThread.ts +13 -2
- package/src/participant/useParticipantThreadController.ts +112 -0
- package/src/participant/useParticipantThreadScroll.ts +13 -3
- package/src/participant/useParticipantUnread.test.tsx +40 -0
- package/src/participant/useParticipantUnread.ts +38 -0
- package/src/participant/useRevealActiveChip.ts +20 -0
- package/src/participant/useScrollAwayState.ts +24 -0
- package/src/participant/useStickToBottom.ts +16 -2
- package/src/styles.css +151 -23
- package/dist/chunk-MRVFFJEH.js +0 -64
- package/dist/chunk-OVFCLRQQ.js +0 -84
|
@@ -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
|
() =>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { useEffect, useReducer } from 'react'
|
|
2
|
+
|
|
3
|
+
import { useAnnounceCopy } from './ParticipantCopyAnnouncer'
|
|
4
|
+
import { INITIAL_PROTOCOL_COPY_STATE, copyProtocolToClipboard, protocolCopyReducer } from './participantProtocolCopy'
|
|
5
|
+
|
|
6
|
+
const COPIED_FEEDBACK_MS = 2000
|
|
7
|
+
|
|
8
|
+
export type CopyFeedback = {
|
|
9
|
+
readonly isCopied: boolean
|
|
10
|
+
readonly copy: (value: string, announcement: string) => void
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Copied state is local to each token; a clipboard failure is silent and shows nothing. */
|
|
14
|
+
export function useParticipantCopyFeedback(): CopyFeedback {
|
|
15
|
+
const [state, dispatch] = useReducer(protocolCopyReducer, INITIAL_PROTOCOL_COPY_STATE)
|
|
16
|
+
const announce = useAnnounceCopy()
|
|
17
|
+
const { isCopied, copyCount } = state
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (!isCopied) return undefined
|
|
21
|
+
const timer = setTimeout(() => dispatch({ type: 'expired' }), COPIED_FEEDBACK_MS)
|
|
22
|
+
return () => clearTimeout(timer)
|
|
23
|
+
}, [isCopied, copyCount])
|
|
24
|
+
|
|
25
|
+
async function copyValue(value: string, announcement: string): Promise<void> {
|
|
26
|
+
const clipboard = typeof navigator === 'undefined' ? undefined : navigator.clipboard
|
|
27
|
+
if (!(await copyProtocolToClipboard(value, clipboard))) return
|
|
28
|
+
dispatch({ type: 'copied' })
|
|
29
|
+
announce(announcement)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return { isCopied, copy: (value, announcement) => void copyValue(value, announcement) }
|
|
33
|
+
}
|
|
@@ -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,8 @@ 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'
|
|
6
|
+
import { useScrollAwayState } from './useScrollAwayState'
|
|
5
7
|
import { useStickToBottom, type ParticipantThreadScroll } from './useStickToBottom'
|
|
6
8
|
|
|
7
9
|
export type UseParticipantThreadScrollResult = {
|
|
@@ -14,13 +16,21 @@ function keyOf(item: ParticipantTimelineItem | undefined): string | undefined {
|
|
|
14
16
|
return item.kind === 'server' ? item.message.id : `pending:${item.pending.clientMessageId}`
|
|
15
17
|
}
|
|
16
18
|
|
|
17
|
-
export function useParticipantThreadScroll(
|
|
18
|
-
|
|
19
|
+
export function useParticipantThreadScroll(
|
|
20
|
+
items: readonly ParticipantTimelineItem[],
|
|
21
|
+
perspective?: ParticipantPerspective,
|
|
22
|
+
): UseParticipantThreadScrollResult {
|
|
23
|
+
const { scroll: base, isNearBottom } = useStickToBottom({
|
|
24
|
+
firstKey: keyOf(items[0]),
|
|
25
|
+
lastKey: keyOf(items[items.length - 1]),
|
|
26
|
+
})
|
|
27
|
+
const { isAwayFromBottom, scrollToLatest } = useScrollAwayState(base.ref, isNearBottom)
|
|
28
|
+
const scroll = { ...base, isAwayFromBottom, scrollToLatest }
|
|
19
29
|
const seenIds = useRef<ReadonlySet<string>>(new Set())
|
|
20
30
|
|
|
21
31
|
useEffect(() => {
|
|
22
32
|
if (isNearBottom) seenIds.current = collectServerMessageIds(items)
|
|
23
33
|
}, [isNearBottom, items])
|
|
24
34
|
|
|
25
|
-
return { scroll, newMessagesCount: isNearBottom ? 0 : countNewIncomingMessages(seenIds.current, items) }
|
|
35
|
+
return { scroll, newMessagesCount: isNearBottom ? 0 : countNewIncomingMessages(seenIds.current, items, perspective) }
|
|
26
36
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test'
|
|
2
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
3
|
+
|
|
4
|
+
import type { ParticipantMessage } from '@adatechnology/conversation-contracts'
|
|
5
|
+
|
|
6
|
+
import { buildMessage } from './participantFixtures.test-helper'
|
|
7
|
+
import type { ParticipantPerspective } from './participantPerspective'
|
|
8
|
+
import { useParticipantUnread } from './useParticipantUnread'
|
|
9
|
+
|
|
10
|
+
const MESSAGES: readonly ParticipantMessage[] = [
|
|
11
|
+
buildMessage({ id: 'a', direction: 'inbound' }),
|
|
12
|
+
buildMessage({ id: 'b', direction: 'inbound' }),
|
|
13
|
+
buildMessage({ id: 'c', direction: 'outbound' }),
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
type ProbeProps = { readonly perspective?: ParticipantPerspective; readonly reported: number }
|
|
17
|
+
|
|
18
|
+
function Probe({ perspective, reported }: ProbeProps) {
|
|
19
|
+
const { unreadCount } = useParticipantUnread({ messages: MESSAGES, perspective, reportedUnreadCount: reported })
|
|
20
|
+
return <output>{unreadCount}</output>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function unreadOf(props: ProbeProps): string {
|
|
24
|
+
return renderToStaticMarkup(<Probe {...props} />)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('useParticipantUnread', () => {
|
|
28
|
+
it('keeps the count the inbox reported when no perspective is given', () => {
|
|
29
|
+
expect(unreadOf({ reported: 3 })).toBe('<output>3</output>')
|
|
30
|
+
expect(unreadOf({ reported: 0 })).toBe('<output>0</output>')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('derives from the messages for the operator: only the inbound ones, whatever the inbox said', () => {
|
|
34
|
+
expect(unreadOf({ perspective: 'operator', reported: 9 })).toBe('<output>2</output>')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('derives from the messages for the participant: only the outbound ones', () => {
|
|
38
|
+
expect(unreadOf({ perspective: 'participant', reported: 9 })).toBe('<output>1</output>')
|
|
39
|
+
})
|
|
40
|
+
})
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { useCallback, useRef, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
import type { ParticipantMessage, ParticipantSubjectRef } from '@adatechnology/conversation-contracts'
|
|
4
|
+
|
|
5
|
+
import { countOtherSideMessages, resolveViewerUnread, type ParticipantPerspective } from './participantPerspective'
|
|
6
|
+
|
|
7
|
+
export type UseParticipantUnreadParams = {
|
|
8
|
+
readonly messages: readonly ParticipantMessage[]
|
|
9
|
+
/** Absent keeps the count the inbox reported, which is how the list-driven screen works. */
|
|
10
|
+
readonly perspective?: ParticipantPerspective
|
|
11
|
+
readonly reportedUnreadCount: number
|
|
12
|
+
readonly onMarkedRead?: (subject: ParticipantSubjectRef) => void
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type UseParticipantUnreadResult = {
|
|
16
|
+
readonly unreadCount: number
|
|
17
|
+
readonly handleMarkedRead: (subject: ParticipantSubjectRef) => void
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** A single conversation has no inbox, so what the viewer has not read is derived from the messages themselves. */
|
|
21
|
+
export function useParticipantUnread(params: UseParticipantUnreadParams): UseParticipantUnreadResult {
|
|
22
|
+
const { messages, perspective, reportedUnreadCount, onMarkedRead } = params
|
|
23
|
+
const [acknowledgedCount, setAcknowledgedCount] = useState(0)
|
|
24
|
+
const messagesRef = useRef(messages)
|
|
25
|
+
messagesRef.current = messages
|
|
26
|
+
|
|
27
|
+
const handleMarkedRead = useCallback(
|
|
28
|
+
(subject: ParticipantSubjectRef): void => {
|
|
29
|
+
if (perspective) setAcknowledgedCount(countOtherSideMessages(messagesRef.current, perspective))
|
|
30
|
+
onMarkedRead?.(subject)
|
|
31
|
+
},
|
|
32
|
+
[perspective, onMarkedRead],
|
|
33
|
+
)
|
|
34
|
+
const unreadCount = perspective
|
|
35
|
+
? resolveViewerUnread({ messages, perspective, acknowledgedCount })
|
|
36
|
+
: reportedUnreadCount
|
|
37
|
+
return { unreadCount, handleMarkedRead }
|
|
38
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { useEffect, useRef, type RefObject } from 'react'
|
|
2
|
+
|
|
3
|
+
import { readPrefersReducedMotion, scrollIntoViewOptions } from './participantScrollView'
|
|
4
|
+
|
|
5
|
+
/** Brings the pressed chip into view when the active one changes; the first render never scrolls. */
|
|
6
|
+
export function useRevealActiveChip(active: string): RefObject<HTMLDivElement | null> {
|
|
7
|
+
const strip = useRef<HTMLDivElement | null>(null)
|
|
8
|
+
const previousActive = useRef<string | undefined>(undefined)
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
const wasActive = previousActive.current
|
|
12
|
+
previousActive.current = active
|
|
13
|
+
if (wasActive === undefined || wasActive === active) return
|
|
14
|
+
strip.current
|
|
15
|
+
?.querySelector<HTMLElement>('[aria-pressed="true"]')
|
|
16
|
+
?.scrollIntoView(scrollIntoViewOptions(readPrefersReducedMotion()))
|
|
17
|
+
}, [active])
|
|
18
|
+
|
|
19
|
+
return strip
|
|
20
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useCallback, type RefObject } from 'react'
|
|
2
|
+
|
|
3
|
+
import { readPrefersReducedMotion, scrollToLatestOptions } from './participantScrollView'
|
|
4
|
+
|
|
5
|
+
export type UseScrollAwayStateResult = {
|
|
6
|
+
readonly isAwayFromBottom: boolean
|
|
7
|
+
readonly scrollToLatest: () => void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Away is the negation of the near-the-end state useStickToBottom already tracks with shouldStickToBottom. */
|
|
11
|
+
export function useScrollAwayState(
|
|
12
|
+
ref: RefObject<HTMLDivElement | null>,
|
|
13
|
+
isNearBottom: boolean,
|
|
14
|
+
): UseScrollAwayStateResult {
|
|
15
|
+
const scrollToLatest = useCallback((): void => {
|
|
16
|
+
const element = ref.current
|
|
17
|
+
if (!element) return
|
|
18
|
+
element.scrollTo(
|
|
19
|
+
scrollToLatestOptions({ scrollHeight: element.scrollHeight, isReducedMotion: readPrefersReducedMotion() }),
|
|
20
|
+
)
|
|
21
|
+
}, [ref])
|
|
22
|
+
|
|
23
|
+
return { isAwayFromBottom: !isNearBottom, scrollToLatest }
|
|
24
|
+
}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { useCallback, useLayoutEffect, useRef, useState, type RefObject, type UIEvent } from 'react'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
NEAR_BOTTOM_THRESHOLD_PX,
|
|
5
|
+
resolveScrollAction,
|
|
6
|
+
scrollAnchorAfterPrepend,
|
|
7
|
+
shouldStickToBottom,
|
|
8
|
+
} from './participantScroll'
|
|
4
9
|
|
|
5
10
|
export type UseStickToBottomParams = {
|
|
6
11
|
readonly firstKey: string | undefined
|
|
@@ -10,6 +15,9 @@ export type UseStickToBottomParams = {
|
|
|
10
15
|
export type ParticipantThreadScroll = {
|
|
11
16
|
readonly ref: RefObject<HTMLDivElement | null>
|
|
12
17
|
readonly onScroll: (event: UIEvent<HTMLDivElement>) => void
|
|
18
|
+
/** Absent draws no scroll-to-latest button. */
|
|
19
|
+
readonly isAwayFromBottom?: boolean
|
|
20
|
+
readonly scrollToLatest?: () => void
|
|
13
21
|
}
|
|
14
22
|
|
|
15
23
|
export type UseStickToBottomResult = {
|
|
@@ -27,7 +35,13 @@ type Snapshot = {
|
|
|
27
35
|
|
|
28
36
|
export function useStickToBottom({ firstKey, lastKey }: UseStickToBottomParams): UseStickToBottomResult {
|
|
29
37
|
const ref = useRef<HTMLDivElement | null>(null)
|
|
30
|
-
const snapshot = useRef<Snapshot>({
|
|
38
|
+
const snapshot = useRef<Snapshot>({
|
|
39
|
+
hasPositioned: false,
|
|
40
|
+
firstKey: undefined,
|
|
41
|
+
lastKey: undefined,
|
|
42
|
+
scrollTop: 0,
|
|
43
|
+
scrollHeight: 0,
|
|
44
|
+
})
|
|
31
45
|
const [isNearBottom, setIsNearBottom] = useState(true)
|
|
32
46
|
const isNearBottomRef = useRef(true)
|
|
33
47
|
|