@linktr.ee/messaging-react 2.0.0 → 2.0.1-rc-1778656305
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Card-CAC3fPjy.js +107 -0
- package/dist/Card-CAC3fPjy.js.map +1 -0
- package/dist/Card-DLUBUg_w.js +132 -0
- package/dist/Card-DLUBUg_w.js.map +1 -0
- package/dist/Card-_StSlnYh.js +163 -0
- package/dist/Card-_StSlnYh.js.map +1 -0
- package/dist/LockedThumbnail-p5RsFOug.js +220 -0
- package/dist/LockedThumbnail-p5RsFOug.js.map +1 -0
- package/dist/assets/index.css +1 -1
- package/dist/{index-Brz9orsI.js → index-B1h46F9x.js} +811 -772
- package/dist/index-B1h46F9x.js.map +1 -0
- package/dist/index.d.ts +87 -28
- package/dist/index.js +3 -3
- package/package.json +1 -1
- package/src/components/ChannelView.test.tsx +11 -0
- package/src/components/ChannelView.tsx +35 -32
- package/src/components/CustomMessage/index.tsx +2 -3
- package/src/components/CustomTypingIndicator/CustomTypingIndicator.stories.tsx +57 -17
- package/src/components/CustomTypingIndicator/CustomTypingIndicator.test.tsx +187 -0
- package/src/components/CustomTypingIndicator/DmAgentContext.ts +3 -0
- package/src/components/CustomTypingIndicator/index.tsx +101 -37
- package/src/components/LockedAttachment/LockedAttachment.stories.tsx +230 -89
- package/src/components/LockedAttachment/components/Composer/Card.tsx +221 -0
- package/src/components/LockedAttachment/components/Composer/index.ts +2 -0
- package/src/components/LockedAttachment/components/Received/Card.tsx +191 -0
- package/src/components/LockedAttachment/components/Received/CardActions.tsx +91 -0
- package/src/components/LockedAttachment/components/Received/index.ts +2 -0
- package/src/components/LockedAttachment/components/Sent/Card.tsx +177 -0
- package/src/components/LockedAttachment/components/Sent/index.ts +2 -0
- package/src/components/LockedAttachment/components/_shared/CardBody.tsx +94 -0
- package/src/components/LockedAttachment/components/_shared/GalleryThumbnail.tsx +178 -0
- package/src/components/LockedAttachment/components/_shared/LockBadge.tsx +39 -0
- package/src/components/LockedAttachment/components/_shared/LockedCardShell.tsx +36 -0
- package/src/components/LockedAttachment/components/_shared/LockedThumbnail.tsx +128 -0
- package/src/components/LockedAttachment/index.tsx +43 -12
- package/src/components/LockedAttachment/types.ts +17 -0
- package/src/components/MediaMessage/index.tsx +2 -2
- package/src/index.ts +6 -1
- package/src/styles.css +7 -0
- package/dist/Card-BHknCeHw.js +0 -138
- package/dist/Card-BHknCeHw.js.map +0 -1
- package/dist/Card-DT7_ms2p.js +0 -127
- package/dist/Card-DT7_ms2p.js.map +0 -1
- package/dist/index-Brz9orsI.js.map +0 -1
- package/src/components/LockedAttachment/components/Creator/Card.tsx +0 -210
- package/src/components/LockedAttachment/components/Creator/index.tsx +0 -2
- package/src/components/LockedAttachment/components/Visitor/Card.tsx +0 -155
- package/src/components/LockedAttachment/components/Visitor/CardActions.tsx +0 -62
- package/src/components/LockedAttachment/components/Visitor/LockBadge.tsx +0 -12
- package/src/components/LockedAttachment/components/Visitor/index.ts +0 -2
package/dist/index.d.ts
CHANGED
|
@@ -241,15 +241,38 @@ export declare interface ChannelViewProps {
|
|
|
241
241
|
sendButton?: ComponentType<any>;
|
|
242
242
|
}
|
|
243
243
|
|
|
244
|
-
export declare interface
|
|
244
|
+
export declare interface ComposerCardProps extends LockedAttachmentBaseProps {
|
|
245
|
+
/** Placeholder shown in the title slot before the composer types one. */
|
|
245
246
|
placeholderTitle?: string;
|
|
247
|
+
/** Placeholder shown in the amount slot before one is configured. */
|
|
246
248
|
placeholderAmountText?: string;
|
|
247
|
-
|
|
249
|
+
/**
|
|
250
|
+
* When provided, renders a dismiss X in the thumbnail corner. Called when
|
|
251
|
+
* the composer clicks it to remove the attachment.
|
|
252
|
+
*/
|
|
248
253
|
onDismiss?: () => void;
|
|
254
|
+
/** Fired the first time the composer taps the thumbnail to preview. */
|
|
249
255
|
onPreviewClick?: () => void;
|
|
256
|
+
/**
|
|
257
|
+
* Lazily loads the underlying source so the composer can preview the
|
|
258
|
+
* attachment they're about to send. Called the first time the thumbnail is
|
|
259
|
+
* tapped; the returned source is cached and reused on subsequent toggles.
|
|
260
|
+
*/
|
|
250
261
|
onFetchSource?: () => Promise<LockedAttachmentSource | void>;
|
|
262
|
+
/**
|
|
263
|
+
* When provided, renders a pencil button in the body bottom-right that the
|
|
264
|
+
* composer can use to edit the attachment metadata (e.g. open the price /
|
|
265
|
+
* gallery editor). Matches the Composer "Button" instance in Figma.
|
|
266
|
+
*/
|
|
267
|
+
onEditClick?: () => void;
|
|
251
268
|
}
|
|
252
269
|
|
|
270
|
+
/**
|
|
271
|
+
* @deprecated Renamed to `SentCardProps`. Drafting usages (with `onDismiss`)
|
|
272
|
+
* should migrate to `ComposerCardProps`.
|
|
273
|
+
*/
|
|
274
|
+
export declare type CreatorCardProps = SentCardProps;
|
|
275
|
+
|
|
253
276
|
export declare const CustomMessageProvider: Provider<Partial<CustomMessageRegistry>>;
|
|
254
277
|
|
|
255
278
|
export declare interface CustomMessageRegistry {
|
|
@@ -301,8 +324,11 @@ export declare function getMessageDisplayText({ message, viewerLanguage, }: {
|
|
|
301
324
|
export declare function isLinkAttachment(a: Attachment): boolean;
|
|
302
325
|
|
|
303
326
|
export declare const LockedAttachment: {
|
|
304
|
-
|
|
305
|
-
|
|
327
|
+
Composer: (props: ComposerCardProps) => JSX_2.Element;
|
|
328
|
+
Sent: (props: SentCardProps) => JSX_2.Element;
|
|
329
|
+
Received: (props: ReceivedCardProps) => JSX_2.Element;
|
|
330
|
+
Creator: (props: SentCardProps) => JSX_2.Element;
|
|
331
|
+
Visitor: (props: ReceivedCardProps) => JSX_2.Element;
|
|
306
332
|
};
|
|
307
333
|
|
|
308
334
|
declare interface LockedAttachmentBaseProps {
|
|
@@ -312,6 +338,14 @@ declare interface LockedAttachmentBaseProps {
|
|
|
312
338
|
detail?: string;
|
|
313
339
|
amountText?: string;
|
|
314
340
|
paymentStatus?: PaymentStatus;
|
|
341
|
+
/**
|
|
342
|
+
* When provided with 2+ items, the card renders as a mixed-media carousel
|
|
343
|
+
* (e.g. a couple of photos + a video) instead of a single thumbnail. Each
|
|
344
|
+
* item brings its own thumbnail and optional source so that
|
|
345
|
+
* `LockedAttachment.Composer` / `.Sent` / `.Received` can all share the
|
|
346
|
+
* same carousel chrome.
|
|
347
|
+
*/
|
|
348
|
+
gallery?: LockedAttachmentGalleryItem[];
|
|
315
349
|
}
|
|
316
350
|
|
|
317
351
|
export declare interface LockedAttachmentContextValue {
|
|
@@ -321,6 +355,15 @@ export declare interface LockedAttachmentContextValue {
|
|
|
321
355
|
onFetchSource?: (message: LocalMessage, channel: Channel) => Promise<LockedAttachmentSource | void>;
|
|
322
356
|
}
|
|
323
357
|
|
|
358
|
+
export declare interface LockedAttachmentGalleryItem {
|
|
359
|
+
/** MIME type of this carousel item — drives the per-item play / lock affordance. */
|
|
360
|
+
mimeType: string;
|
|
361
|
+
/** Poster image used for both the locked (blurred) and unlocked preview state. */
|
|
362
|
+
thumbnailUrl?: string;
|
|
363
|
+
/** Underlying source (image or video URL) — shown only when unlocked. */
|
|
364
|
+
sourceUrl?: string;
|
|
365
|
+
}
|
|
366
|
+
|
|
324
367
|
export declare interface LockedAttachmentSource {
|
|
325
368
|
/** Proxied URL used by the media player for in-app playback. */
|
|
326
369
|
sourceUrl: string;
|
|
@@ -501,10 +544,48 @@ export declare interface Participant {
|
|
|
501
544
|
*/
|
|
502
545
|
declare type PaymentStatus = 'pending' | 'paid' | 'failed' | 'refunded';
|
|
503
546
|
|
|
547
|
+
export declare interface ReceivedCardProps extends LockedAttachmentBaseProps {
|
|
548
|
+
/**
|
|
549
|
+
* Called when the recipient clicks Unlock on an unpaid attachment.
|
|
550
|
+
* Use this to open a checkout flow. Omit to hide the Unlock button.
|
|
551
|
+
*/
|
|
552
|
+
onUnlockClick?: () => void;
|
|
553
|
+
/**
|
|
554
|
+
* Called to fetch the attachment source — fired automatically when
|
|
555
|
+
* `paymentStatus` transitions to `'paid'`, or immediately on click when
|
|
556
|
+
* `paymentStatus` is already `'paid'`. Return a `LockedAttachmentSource`
|
|
557
|
+
* to unlock the card.
|
|
558
|
+
*/
|
|
559
|
+
onFetchSource?: () => Promise<LockedAttachmentSource | void>;
|
|
560
|
+
/**
|
|
561
|
+
* Called when the recipient clicks Download on an unlocked card.
|
|
562
|
+
* Omit to hide the Download button.
|
|
563
|
+
*/
|
|
564
|
+
onDownloadClick?: () => void;
|
|
565
|
+
/**
|
|
566
|
+
* When true, shows a loading spinner on the Unlock button.
|
|
567
|
+
* Driven by the LockedAttachmentContext (e.g. checkout in progress).
|
|
568
|
+
*/
|
|
569
|
+
isUnlocking?: boolean;
|
|
570
|
+
}
|
|
571
|
+
|
|
504
572
|
export declare function resolveLinkAttachment(message: LocalMessage): Attachment | undefined;
|
|
505
573
|
|
|
506
574
|
export declare function resolveMediaFromMessage(message: LocalMessage): MediaMessageResolved | null;
|
|
507
575
|
|
|
576
|
+
export declare interface SentCardProps extends LockedAttachmentBaseProps {
|
|
577
|
+
/** Placeholder shown in the title slot when no title is set. */
|
|
578
|
+
placeholderTitle?: string;
|
|
579
|
+
/** Fired the first time the sender taps the thumbnail to preview their own attachment. */
|
|
580
|
+
onPreviewClick?: () => void;
|
|
581
|
+
/**
|
|
582
|
+
* Lazily loads the underlying source so the sender can preview the attachment.
|
|
583
|
+
* Called the first time the thumbnail is tapped; the returned source is cached
|
|
584
|
+
* and reused on subsequent toggles.
|
|
585
|
+
*/
|
|
586
|
+
onFetchSource?: () => Promise<LockedAttachmentSource | void>;
|
|
587
|
+
}
|
|
588
|
+
|
|
508
589
|
export declare function useCustomMessage<K extends keyof CustomMessageRegistry>(key: K): CustomMessageRegistry[K];
|
|
509
590
|
|
|
510
591
|
/**
|
|
@@ -528,30 +609,8 @@ declare interface UseMessageVoteResult {
|
|
|
528
609
|
*/
|
|
529
610
|
export declare const useMessaging: () => MessagingContextValue;
|
|
530
611
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
* Called when the visitor clicks Unlock on an unpaid attachment.
|
|
534
|
-
* Use this to open a checkout flow. Omit to hide the Unlock button.
|
|
535
|
-
*/
|
|
536
|
-
onUnlockClick?: () => void;
|
|
537
|
-
/**
|
|
538
|
-
* Called to fetch the attachment source — fired automatically when
|
|
539
|
-
* paymentStatus transitions to 'paid', or immediately on click when
|
|
540
|
-
* paymentStatus is already 'paid'. Return a LockedAttachmentSource to
|
|
541
|
-
* unlock the card.
|
|
542
|
-
*/
|
|
543
|
-
onFetchSource?: () => Promise<LockedAttachmentSource | void>;
|
|
544
|
-
/**
|
|
545
|
-
* Called when the visitor clicks Download on an unlocked card.
|
|
546
|
-
* Omit to hide the Download button.
|
|
547
|
-
*/
|
|
548
|
-
onDownloadClick?: () => void;
|
|
549
|
-
/**
|
|
550
|
-
* When true, shows loading dots on the Unlock button.
|
|
551
|
-
* Driven by the LockedAttachmentContext (e.g. checkout in progress, payment processing).
|
|
552
|
-
*/
|
|
553
|
-
isUnlocking?: boolean;
|
|
554
|
-
}
|
|
612
|
+
/** @deprecated Renamed to `ReceivedCardProps`. */
|
|
613
|
+
export declare type VisitorCardProps = ReceivedCardProps;
|
|
555
614
|
|
|
556
615
|
export declare type VoteSelection = 'up' | 'down' | null;
|
|
557
616
|
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { a as e, b as t, C as i, c as n, d as o, e as g, F as m, f as M, L as r, M as l, h as u, i as h, j as L, k as d, l as C, m as c, n as v, o as A, p as k, u as p, q as F, s as f } from "./index-B1h46F9x.js";
|
|
2
2
|
export {
|
|
3
3
|
e as ActionButton,
|
|
4
4
|
t as Avatar,
|
|
@@ -7,8 +7,8 @@ export {
|
|
|
7
7
|
o as ChannelView,
|
|
8
8
|
g as CustomMessageProvider,
|
|
9
9
|
m as FaqList,
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
M as FaqListItem,
|
|
11
|
+
r as LockedAttachment,
|
|
12
12
|
l as MediaMessage,
|
|
13
13
|
u as MessageVoteButtons,
|
|
14
14
|
h as MessagingProvider,
|
package/package.json
CHANGED
|
@@ -28,6 +28,17 @@ vi.mock('stream-chat-react', () => ({
|
|
|
28
28
|
),
|
|
29
29
|
useMessageContext: () => ({ message: { id: 'message-1', text: 'hello' } }),
|
|
30
30
|
useChannelStateContext: () => ({ channel: activeChannel }),
|
|
31
|
+
useChatContext: () => ({ client: { user: { id: 'visitor-1' } } }),
|
|
32
|
+
useTypingContext: () => ({ typing: {} }),
|
|
33
|
+
useAIState: () => ({ aiState: 'AI_STATE_IDLE' }),
|
|
34
|
+
AIStates: {
|
|
35
|
+
Error: 'AI_STATE_ERROR',
|
|
36
|
+
ExternalSources: 'AI_STATE_EXTERNAL_SOURCES',
|
|
37
|
+
Generating: 'AI_STATE_GENERATING',
|
|
38
|
+
Idle: 'AI_STATE_IDLE',
|
|
39
|
+
Stop: 'AI_STATE_STOP',
|
|
40
|
+
Thinking: 'AI_STATE_THINKING',
|
|
41
|
+
},
|
|
31
42
|
}))
|
|
32
43
|
|
|
33
44
|
vi.mock('../providers/MessagingProvider', () => ({
|
|
@@ -28,6 +28,7 @@ import { CustomMessage } from './CustomMessage'
|
|
|
28
28
|
import { CustomMessageInput } from './CustomMessageInput'
|
|
29
29
|
import { CustomSystemMessage } from './CustomSystemMessage'
|
|
30
30
|
import CustomTypingIndicator from './CustomTypingIndicator'
|
|
31
|
+
import { DmAgentEnabledContext } from './CustomTypingIndicator/DmAgentContext'
|
|
31
32
|
import { ChannelEmptyState } from './MessagingShell/ChannelEmptyState'
|
|
32
33
|
import { LoadingState } from './MessagingShell/LoadingState'
|
|
33
34
|
|
|
@@ -495,38 +496,40 @@ export const ChannelView = React.memo<ChannelViewProps>(
|
|
|
495
496
|
className
|
|
496
497
|
)}
|
|
497
498
|
>
|
|
498
|
-
<
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
499
|
+
<DmAgentEnabledContext.Provider value={dmAgentEnabled ?? false}>
|
|
500
|
+
<Channel
|
|
501
|
+
channel={channel}
|
|
502
|
+
MessageSystem={CustomSystemMessage}
|
|
503
|
+
EmptyStateIndicator={CustomChannelEmptyState}
|
|
504
|
+
LoadingIndicator={LoadingState}
|
|
505
|
+
DateSeparator={CustomDateSeparator}
|
|
506
|
+
TypingIndicator={CustomTypingIndicator}
|
|
507
|
+
doSendMessageRequest={doSendMessageRequest}
|
|
508
|
+
{...(sendButton ? { SendButton: sendButton } : {})}
|
|
509
|
+
>
|
|
510
|
+
<ChannelViewInner
|
|
511
|
+
onBack={onBack}
|
|
512
|
+
showBackButton={showBackButton}
|
|
513
|
+
renderMessageInputActions={renderMessageInputActions}
|
|
514
|
+
renderConversationFooter={renderConversationFooter}
|
|
515
|
+
onLeaveConversation={onLeaveConversation}
|
|
516
|
+
onBlockParticipant={onBlockParticipant}
|
|
517
|
+
CustomChannelEmptyState={CustomChannelEmptyState}
|
|
518
|
+
showDeleteConversation={showDeleteConversation}
|
|
519
|
+
onDeleteConversationClick={onDeleteConversationClick}
|
|
520
|
+
onBlockParticipantClick={onBlockParticipantClick}
|
|
521
|
+
onReportParticipantClick={onReportParticipantClick}
|
|
522
|
+
showStarButton={showStarButton}
|
|
523
|
+
dmAgentEnabled={dmAgentEnabled}
|
|
524
|
+
chatbotVotingEnabled={chatbotVotingEnabled}
|
|
525
|
+
renderChannelBanner={renderChannelBanner}
|
|
526
|
+
customProfileContent={customProfileContent}
|
|
527
|
+
customChannelActions={customChannelActions}
|
|
528
|
+
renderMessage={renderMessage}
|
|
529
|
+
viewerLanguage={viewerLanguage}
|
|
530
|
+
/>
|
|
531
|
+
</Channel>
|
|
532
|
+
</DmAgentEnabledContext.Provider>
|
|
530
533
|
</div>
|
|
531
534
|
)
|
|
532
535
|
}
|
|
@@ -227,21 +227,20 @@ const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
|
|
|
227
227
|
{isAttachment ? (
|
|
228
228
|
<div className="str-chat__message-bubble-wrapper">
|
|
229
229
|
{isMine ? (
|
|
230
|
-
<LockedAttachment.
|
|
230
|
+
<LockedAttachment.Sent
|
|
231
231
|
title={message.metadata?.attachment_title}
|
|
232
232
|
mimeType={message.metadata?.attachment_mime_type}
|
|
233
233
|
thumbnailUrl={message.metadata?.attachment_thumbnail}
|
|
234
234
|
amountText={message.metadata?.amount_text}
|
|
235
235
|
detail={message.metadata?.attachment_detail}
|
|
236
236
|
paymentStatus={message.metadata?.payment_status}
|
|
237
|
-
isUnlocking={isUnlocking(message.id)}
|
|
238
237
|
onPreviewClick={() => onUnlockClick?.(message, channel)}
|
|
239
238
|
onFetchSource={async () =>
|
|
240
239
|
await onFetchSource?.(message, channel)
|
|
241
240
|
}
|
|
242
241
|
/>
|
|
243
242
|
) : (
|
|
244
|
-
<LockedAttachment.
|
|
243
|
+
<LockedAttachment.Received
|
|
245
244
|
title={message.metadata?.attachment_title}
|
|
246
245
|
mimeType={message.metadata?.attachment_mime_type}
|
|
247
246
|
thumbnailUrl={message.metadata?.attachment_thumbnail}
|
|
@@ -2,16 +2,21 @@ import type { Meta, StoryFn } from '@storybook/react'
|
|
|
2
2
|
import React from 'react'
|
|
3
3
|
import type { Event } from 'stream-chat'
|
|
4
4
|
import {
|
|
5
|
+
AIStates,
|
|
5
6
|
ChannelStateProvider,
|
|
6
7
|
ChatProvider,
|
|
7
8
|
TypingProvider,
|
|
8
9
|
} from 'stream-chat-react'
|
|
9
10
|
|
|
11
|
+
import { DmAgentEnabledContext } from './DmAgentContext'
|
|
12
|
+
|
|
10
13
|
import CustomTypingIndicator from '.'
|
|
11
14
|
|
|
12
15
|
type StoryProps = {
|
|
13
16
|
typingEventsEnabled?: boolean
|
|
14
17
|
typing?: Record<string, Event>
|
|
18
|
+
aiState?: string
|
|
19
|
+
dmAgentEnabled?: boolean
|
|
15
20
|
}
|
|
16
21
|
|
|
17
22
|
const currentUser = {
|
|
@@ -33,10 +38,38 @@ const defaultTyping: Record<string, Event> = {
|
|
|
33
38
|
} as Event,
|
|
34
39
|
}
|
|
35
40
|
|
|
41
|
+
type ListenerMap = Record<string, (event: { ai_state: string; cid: string }) => void>
|
|
42
|
+
|
|
43
|
+
const createMockChannel = ({ aiState }: { aiState?: string }) => {
|
|
44
|
+
const listeners: ListenerMap = {}
|
|
45
|
+
const channel = {
|
|
46
|
+
cid: 'messaging:test',
|
|
47
|
+
state: {
|
|
48
|
+
members: {
|
|
49
|
+
[currentUser.id]: { user: currentUser },
|
|
50
|
+
[typingUser.id]: { user: typingUser },
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
on(eventType: string, handler: ListenerMap[string]) {
|
|
54
|
+
listeners[eventType] = handler
|
|
55
|
+
// Fire the requested AI state synchronously so the hook picks it up.
|
|
56
|
+
if (eventType === 'ai_indicator.update' && aiState) {
|
|
57
|
+
handler({ ai_state: aiState, cid: 'messaging:test' })
|
|
58
|
+
}
|
|
59
|
+
return { unsubscribe: () => {} }
|
|
60
|
+
},
|
|
61
|
+
}
|
|
62
|
+
return channel
|
|
63
|
+
}
|
|
64
|
+
|
|
36
65
|
const StoryWrapper: React.FC<StoryProps> = ({
|
|
37
66
|
typingEventsEnabled = true,
|
|
38
|
-
typing =
|
|
67
|
+
typing = {},
|
|
68
|
+
aiState,
|
|
69
|
+
dmAgentEnabled = true,
|
|
39
70
|
}) => {
|
|
71
|
+
const channel = createMockChannel({ aiState })
|
|
72
|
+
|
|
40
73
|
const chatContextValue = {
|
|
41
74
|
client: {
|
|
42
75
|
user: currentUser,
|
|
@@ -57,18 +90,7 @@ const StoryWrapper: React.FC<StoryProps> = ({
|
|
|
57
90
|
}
|
|
58
91
|
|
|
59
92
|
const channelStateValue = {
|
|
60
|
-
channel
|
|
61
|
-
state: {
|
|
62
|
-
members: {
|
|
63
|
-
[currentUser.id]: {
|
|
64
|
-
user: currentUser,
|
|
65
|
-
},
|
|
66
|
-
[typingUser.id]: {
|
|
67
|
-
user: typingUser,
|
|
68
|
-
},
|
|
69
|
-
},
|
|
70
|
-
},
|
|
71
|
-
},
|
|
93
|
+
channel,
|
|
72
94
|
channelCapabilities: {},
|
|
73
95
|
channelConfig: {
|
|
74
96
|
typing_events: typingEventsEnabled,
|
|
@@ -85,9 +107,11 @@ const StoryWrapper: React.FC<StoryProps> = ({
|
|
|
85
107
|
<ChatProvider value={chatContextValue as never}>
|
|
86
108
|
<ChannelStateProvider value={channelStateValue as never}>
|
|
87
109
|
<TypingProvider value={{ typing }}>
|
|
88
|
-
<
|
|
89
|
-
<
|
|
90
|
-
|
|
110
|
+
<DmAgentEnabledContext.Provider value={dmAgentEnabled}>
|
|
111
|
+
<div className="relative h-20 w-[200px] bg-[#f4f4f4] p-3">
|
|
112
|
+
<CustomTypingIndicator />
|
|
113
|
+
</div>
|
|
114
|
+
</DmAgentEnabledContext.Provider>
|
|
91
115
|
</TypingProvider>
|
|
92
116
|
</ChannelStateProvider>
|
|
93
117
|
</ChatProvider>
|
|
@@ -104,7 +128,9 @@ const meta: Meta<StoryProps> = {
|
|
|
104
128
|
export default meta
|
|
105
129
|
|
|
106
130
|
export const Default: StoryFn<StoryProps> = (args) => <StoryWrapper {...args} />
|
|
107
|
-
Default.args = {
|
|
131
|
+
Default.args = {
|
|
132
|
+
typing: defaultTyping,
|
|
133
|
+
}
|
|
108
134
|
|
|
109
135
|
export const HiddenWhenNoTyping: StoryFn<StoryProps> = (args) => (
|
|
110
136
|
<StoryWrapper {...args} />
|
|
@@ -112,3 +138,17 @@ export const HiddenWhenNoTyping: StoryFn<StoryProps> = (args) => (
|
|
|
112
138
|
HiddenWhenNoTyping.args = {
|
|
113
139
|
typing: {},
|
|
114
140
|
}
|
|
141
|
+
|
|
142
|
+
export const AiAgentThinking: StoryFn<StoryProps> = (args) => (
|
|
143
|
+
<StoryWrapper {...args} />
|
|
144
|
+
)
|
|
145
|
+
AiAgentThinking.args = {
|
|
146
|
+
aiState: AIStates.Thinking,
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export const AiAgentGenerating: StoryFn<StoryProps> = (args) => (
|
|
150
|
+
<StoryWrapper {...args} />
|
|
151
|
+
)
|
|
152
|
+
AiAgentGenerating.args = {
|
|
153
|
+
aiState: AIStates.Generating,
|
|
154
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import type { Event } from 'stream-chat'
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
4
|
+
|
|
5
|
+
import { renderWithProviders, screen } from '../../test/utils'
|
|
6
|
+
|
|
7
|
+
import { DmAgentEnabledContext } from './DmAgentContext'
|
|
8
|
+
|
|
9
|
+
const visitor = { id: 'visitor-1', name: 'Visitor' }
|
|
10
|
+
const agent = { id: 'creator-1', name: 'Creator', image: 'agent.png' }
|
|
11
|
+
|
|
12
|
+
let typingContext: { typing: Record<string, Event> } = { typing: {} }
|
|
13
|
+
let aiStateContext: { aiState: string } = { aiState: 'AI_STATE_IDLE' }
|
|
14
|
+
let channelStateContext: {
|
|
15
|
+
channel: {
|
|
16
|
+
state: { members: Record<string, { user: typeof visitor | typeof agent }> }
|
|
17
|
+
}
|
|
18
|
+
channelConfig: { typing_events: boolean }
|
|
19
|
+
thread: undefined
|
|
20
|
+
} = {
|
|
21
|
+
channel: {
|
|
22
|
+
state: {
|
|
23
|
+
members: {
|
|
24
|
+
[visitor.id]: { user: visitor },
|
|
25
|
+
[agent.id]: { user: agent },
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
channelConfig: { typing_events: true },
|
|
30
|
+
thread: undefined,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
vi.mock('stream-chat-react', () => ({
|
|
34
|
+
AIStates: {
|
|
35
|
+
Error: 'AI_STATE_ERROR',
|
|
36
|
+
ExternalSources: 'AI_STATE_EXTERNAL_SOURCES',
|
|
37
|
+
Generating: 'AI_STATE_GENERATING',
|
|
38
|
+
Idle: 'AI_STATE_IDLE',
|
|
39
|
+
Stop: 'AI_STATE_STOP',
|
|
40
|
+
Thinking: 'AI_STATE_THINKING',
|
|
41
|
+
},
|
|
42
|
+
useAIState: () => aiStateContext,
|
|
43
|
+
useChannelStateContext: () => channelStateContext,
|
|
44
|
+
useChatContext: () => ({ client: { user: visitor } }),
|
|
45
|
+
useTypingContext: () => typingContext,
|
|
46
|
+
}))
|
|
47
|
+
|
|
48
|
+
vi.mock('../Avatar', () => ({
|
|
49
|
+
Avatar: ({ name, id }: { name: string; id: string }) => (
|
|
50
|
+
<div data-testid="avatar" data-id={id}>
|
|
51
|
+
{name}
|
|
52
|
+
</div>
|
|
53
|
+
),
|
|
54
|
+
}))
|
|
55
|
+
|
|
56
|
+
const importIndicator = async () => (await import('.')).default
|
|
57
|
+
|
|
58
|
+
const renderIndicator = async (dmAgentEnabled = true) => {
|
|
59
|
+
const CustomTypingIndicator = await importIndicator()
|
|
60
|
+
return renderWithProviders(
|
|
61
|
+
<DmAgentEnabledContext.Provider value={dmAgentEnabled}>
|
|
62
|
+
<CustomTypingIndicator />
|
|
63
|
+
</DmAgentEnabledContext.Provider>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
describe('CustomTypingIndicator', () => {
|
|
68
|
+
beforeEach(() => {
|
|
69
|
+
typingContext = { typing: {} }
|
|
70
|
+
aiStateContext = { aiState: 'AI_STATE_IDLE' }
|
|
71
|
+
channelStateContext = {
|
|
72
|
+
channel: {
|
|
73
|
+
state: {
|
|
74
|
+
members: {
|
|
75
|
+
[visitor.id]: { user: visitor },
|
|
76
|
+
[agent.id]: { user: agent },
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
channelConfig: { typing_events: true },
|
|
81
|
+
thread: undefined,
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('renders nothing when idle and no typers', async () => {
|
|
86
|
+
await renderIndicator()
|
|
87
|
+
expect(screen.queryByTestId('typing-indicator')).toBeNull()
|
|
88
|
+
expect(screen.queryByTestId('typing-indicator-ai')).toBeNull()
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('renders the human typing bubble when someone else is typing', async () => {
|
|
92
|
+
typingContext = {
|
|
93
|
+
typing: {
|
|
94
|
+
[agent.id]: {
|
|
95
|
+
type: 'typing.start',
|
|
96
|
+
user: agent,
|
|
97
|
+
parent_id: undefined,
|
|
98
|
+
} as Event,
|
|
99
|
+
},
|
|
100
|
+
}
|
|
101
|
+
await renderIndicator()
|
|
102
|
+
expect(screen.getByTestId('typing-indicator')).toBeInTheDocument()
|
|
103
|
+
expect(screen.getByTestId('avatar')).toHaveAttribute('data-id', agent.id)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('hides the human typing bubble when typing_events is disabled', async () => {
|
|
107
|
+
channelStateContext.channelConfig.typing_events = false
|
|
108
|
+
typingContext = {
|
|
109
|
+
typing: {
|
|
110
|
+
[agent.id]: {
|
|
111
|
+
type: 'typing.start',
|
|
112
|
+
user: agent,
|
|
113
|
+
parent_id: undefined,
|
|
114
|
+
} as Event,
|
|
115
|
+
},
|
|
116
|
+
}
|
|
117
|
+
await renderIndicator()
|
|
118
|
+
expect(screen.queryByTestId('typing-indicator')).toBeNull()
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('renders the AI bubble when the agent is thinking', async () => {
|
|
122
|
+
aiStateContext = { aiState: 'AI_STATE_THINKING' }
|
|
123
|
+
await renderIndicator()
|
|
124
|
+
expect(screen.getByTestId('typing-indicator-ai')).toBeInTheDocument()
|
|
125
|
+
expect(screen.getByTestId('avatar')).toHaveAttribute('data-id', agent.id)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('renders the AI bubble when the agent is generating', async () => {
|
|
129
|
+
aiStateContext = { aiState: 'AI_STATE_GENERATING' }
|
|
130
|
+
await renderIndicator()
|
|
131
|
+
expect(screen.getByTestId('typing-indicator-ai')).toBeInTheDocument()
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('renders the AI bubble when the agent is checking external sources', async () => {
|
|
135
|
+
aiStateContext = { aiState: 'AI_STATE_EXTERNAL_SOURCES' }
|
|
136
|
+
await renderIndicator()
|
|
137
|
+
expect(screen.getByTestId('typing-indicator-ai')).toBeInTheDocument()
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('does not render the AI bubble inside a thread list', async () => {
|
|
141
|
+
aiStateContext = { aiState: 'AI_STATE_GENERATING' }
|
|
142
|
+
const CustomTypingIndicator = await importIndicator()
|
|
143
|
+
renderWithProviders(
|
|
144
|
+
<DmAgentEnabledContext.Provider value={true}>
|
|
145
|
+
<CustomTypingIndicator threadList />
|
|
146
|
+
</DmAgentEnabledContext.Provider>
|
|
147
|
+
)
|
|
148
|
+
expect(screen.queryByTestId('typing-indicator-ai')).toBeNull()
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('renders the AI bubble even when typing_events is disabled', async () => {
|
|
152
|
+
channelStateContext.channelConfig.typing_events = false
|
|
153
|
+
aiStateContext = { aiState: 'AI_STATE_THINKING' }
|
|
154
|
+
await renderIndicator()
|
|
155
|
+
expect(screen.getByTestId('typing-indicator-ai')).toBeInTheDocument()
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('falls back to a default avatar id when no other channel member exists', async () => {
|
|
159
|
+
channelStateContext.channel.state.members = {
|
|
160
|
+
[visitor.id]: { user: visitor },
|
|
161
|
+
}
|
|
162
|
+
aiStateContext = { aiState: 'AI_STATE_GENERATING' }
|
|
163
|
+
await renderIndicator()
|
|
164
|
+
expect(screen.getByTestId('typing-indicator-ai')).toBeInTheDocument()
|
|
165
|
+
expect(screen.getByTestId('avatar')).toHaveAttribute('data-id', 'ai-agent')
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it('does not render the AI bubble when dmAgentEnabled is false', async () => {
|
|
169
|
+
aiStateContext = { aiState: 'AI_STATE_GENERATING' }
|
|
170
|
+
await renderIndicator(false)
|
|
171
|
+
expect(screen.queryByTestId('typing-indicator-ai')).toBeNull()
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it('still renders the human bubble when dmAgentEnabled is false', async () => {
|
|
175
|
+
typingContext = {
|
|
176
|
+
typing: {
|
|
177
|
+
[agent.id]: {
|
|
178
|
+
type: 'typing.start',
|
|
179
|
+
user: agent,
|
|
180
|
+
parent_id: undefined,
|
|
181
|
+
} as Event,
|
|
182
|
+
},
|
|
183
|
+
}
|
|
184
|
+
await renderIndicator(false)
|
|
185
|
+
expect(screen.getByTestId('typing-indicator')).toBeInTheDocument()
|
|
186
|
+
})
|
|
187
|
+
})
|