@linktr.ee/messaging-react 1.4.1 → 1.5.0-rc-1761632549
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/assets/index.css +1 -1
- package/dist/index.d.ts +34 -1
- package/dist/index.js +768 -701
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/ChannelView.tsx +76 -32
- package/src/components/FaqList/FaqList.stories.tsx +137 -0
- package/src/components/FaqList/FaqListItem.stories.tsx +35 -0
- package/src/components/FaqList/FaqListItem.tsx +34 -0
- package/src/components/FaqList/index.tsx +50 -0
- package/src/components/MessagingShell/index.tsx +2 -0
- package/src/index.ts +4 -0
- package/src/types.ts +8 -1
|
@@ -370,19 +370,29 @@ const ChannelInfoDialog: React.FC<{
|
|
|
370
370
|
}
|
|
371
371
|
|
|
372
372
|
/**
|
|
373
|
-
*
|
|
373
|
+
* Inner component that has access to channel context
|
|
374
374
|
*/
|
|
375
|
-
|
|
376
|
-
|
|
375
|
+
const ChannelViewInner: React.FC<{
|
|
376
|
+
onBack?: () => void
|
|
377
|
+
showBackButton: boolean
|
|
378
|
+
renderMessageInputActions?: (channel: ChannelType) => React.ReactNode
|
|
379
|
+
onLeaveConversation?: (channel: ChannelType) => void
|
|
380
|
+
onBlockParticipant?: (participantId?: string) => void
|
|
381
|
+
CustomChannelEmptyState?: React.ComponentType
|
|
382
|
+
}> = ({
|
|
377
383
|
onBack,
|
|
378
|
-
showBackButton
|
|
384
|
+
showBackButton,
|
|
379
385
|
renderMessageInputActions,
|
|
380
386
|
onLeaveConversation,
|
|
381
387
|
onBlockParticipant,
|
|
382
|
-
|
|
388
|
+
CustomChannelEmptyState,
|
|
383
389
|
}) => {
|
|
390
|
+
const { channel } = useChannelStateContext()
|
|
384
391
|
const [showInfo, setShowInfo] = useState(false)
|
|
385
392
|
|
|
393
|
+
// Check if channel has messages - using context to reactively subscribe to message updates
|
|
394
|
+
const hasMessages = (channel?.state?.messages?.length ?? 0) > 0
|
|
395
|
+
|
|
386
396
|
// Get participant info for info dialog
|
|
387
397
|
const participant = React.useMemo(() => {
|
|
388
398
|
const members = Object.values(channel.state.members || {})
|
|
@@ -405,35 +415,35 @@ export const ChannelView: React.FC<ChannelViewProps> = ({
|
|
|
405
415
|
}, [channel.data])
|
|
406
416
|
|
|
407
417
|
return (
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
className
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
onBack={onBack}
|
|
420
|
-
showBackButton={showBackButton}
|
|
421
|
-
onShowInfo={() => setShowInfo(true)}
|
|
422
|
-
canShowInfo={Boolean(participant)}
|
|
423
|
-
/>
|
|
424
|
-
</div>
|
|
418
|
+
<>
|
|
419
|
+
<Window>
|
|
420
|
+
{/* Custom Channel Header */}
|
|
421
|
+
<div className="border-b border-sand bg-white px-4 py-3">
|
|
422
|
+
<CustomChannelHeader
|
|
423
|
+
onBack={onBack}
|
|
424
|
+
showBackButton={showBackButton}
|
|
425
|
+
onShowInfo={() => setShowInfo(true)}
|
|
426
|
+
canShowInfo={Boolean(participant)}
|
|
427
|
+
/>
|
|
428
|
+
</div>
|
|
425
429
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
</div>
|
|
430
|
+
{/* Message List */}
|
|
431
|
+
<div className="flex-1 overflow-hidden">
|
|
432
|
+
<MessageList hideDeletedMessages hideNewMessageSeparator={false} />
|
|
430
433
|
|
|
431
|
-
{/*
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
434
|
+
{/* Show custom empty state when no messages */}
|
|
435
|
+
{hasMessages === false && CustomChannelEmptyState && (
|
|
436
|
+
<div className="p-4">
|
|
437
|
+
<CustomChannelEmptyState />
|
|
438
|
+
</div>
|
|
439
|
+
)}
|
|
440
|
+
</div>
|
|
441
|
+
|
|
442
|
+
{/* Message Input */}
|
|
443
|
+
<CustomMessageInput
|
|
444
|
+
renderActions={() => renderMessageInputActions?.(channel)}
|
|
445
|
+
/>
|
|
446
|
+
</Window>
|
|
437
447
|
|
|
438
448
|
{/* Channel Info Dialog */}
|
|
439
449
|
<ChannelInfoDialog
|
|
@@ -445,6 +455,40 @@ export const ChannelView: React.FC<ChannelViewProps> = ({
|
|
|
445
455
|
onLeaveConversation={onLeaveConversation}
|
|
446
456
|
onBlockParticipant={onBlockParticipant}
|
|
447
457
|
/>
|
|
458
|
+
</>
|
|
459
|
+
)
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Channel view component with message list and input
|
|
464
|
+
*/
|
|
465
|
+
export const ChannelView: React.FC<ChannelViewProps> = ({
|
|
466
|
+
channel,
|
|
467
|
+
onBack,
|
|
468
|
+
showBackButton = false,
|
|
469
|
+
renderMessageInputActions,
|
|
470
|
+
onLeaveConversation,
|
|
471
|
+
onBlockParticipant,
|
|
472
|
+
className,
|
|
473
|
+
CustomChannelEmptyState,
|
|
474
|
+
}) => {
|
|
475
|
+
return (
|
|
476
|
+
<div
|
|
477
|
+
className={classNames(
|
|
478
|
+
'messaging-channel-view h-full flex flex-col',
|
|
479
|
+
className
|
|
480
|
+
)}
|
|
481
|
+
>
|
|
482
|
+
<Channel channel={channel}>
|
|
483
|
+
<ChannelViewInner
|
|
484
|
+
onBack={onBack}
|
|
485
|
+
showBackButton={showBackButton}
|
|
486
|
+
renderMessageInputActions={renderMessageInputActions}
|
|
487
|
+
onLeaveConversation={onLeaveConversation}
|
|
488
|
+
onBlockParticipant={onBlockParticipant}
|
|
489
|
+
CustomChannelEmptyState={CustomChannelEmptyState}
|
|
490
|
+
/>
|
|
491
|
+
</Channel>
|
|
448
492
|
</div>
|
|
449
493
|
)
|
|
450
494
|
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import type { Meta, StoryFn } from '@storybook/react'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
|
|
4
|
+
import { FaqList } from './index'
|
|
5
|
+
|
|
6
|
+
type ComponentProps = React.ComponentProps<typeof FaqList>
|
|
7
|
+
|
|
8
|
+
const meta: Meta<ComponentProps> = {
|
|
9
|
+
title: 'FaqList',
|
|
10
|
+
component: FaqList,
|
|
11
|
+
parameters: {
|
|
12
|
+
layout: 'centered',
|
|
13
|
+
},
|
|
14
|
+
}
|
|
15
|
+
export default meta
|
|
16
|
+
|
|
17
|
+
const Template: StoryFn<ComponentProps> = (args) => {
|
|
18
|
+
return (
|
|
19
|
+
<div className="w-96 bg-white">
|
|
20
|
+
<FaqList {...args} />
|
|
21
|
+
</div>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const Default: StoryFn<ComponentProps> = Template.bind({})
|
|
26
|
+
Default.args = {
|
|
27
|
+
faqs: [
|
|
28
|
+
{
|
|
29
|
+
id: '1',
|
|
30
|
+
question: 'Book club slots',
|
|
31
|
+
answer: 'We ship worldwide...',
|
|
32
|
+
enabled: true,
|
|
33
|
+
order: 1,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: '2',
|
|
37
|
+
question: 'Refunds',
|
|
38
|
+
answer: 'You can track...',
|
|
39
|
+
enabled: true,
|
|
40
|
+
order: 2,
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: '3',
|
|
44
|
+
question: 'Brand collabs',
|
|
45
|
+
answer: 'Returns are accepted...',
|
|
46
|
+
enabled: true,
|
|
47
|
+
order: 3,
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
onFaqClick: (id) => console.log('FAQ clicked:', id),
|
|
51
|
+
headerText: 'Tap to send a suggested question by Brie Parson:',
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const WithoutHeader: StoryFn<ComponentProps> = Template.bind({})
|
|
55
|
+
WithoutHeader.args = {
|
|
56
|
+
faqs: [
|
|
57
|
+
{
|
|
58
|
+
id: '1',
|
|
59
|
+
question: 'Book club slots',
|
|
60
|
+
answer: 'We ship worldwide...',
|
|
61
|
+
enabled: true,
|
|
62
|
+
order: 1,
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
id: '2',
|
|
66
|
+
question: 'Refunds',
|
|
67
|
+
answer: 'You can track...',
|
|
68
|
+
enabled: true,
|
|
69
|
+
order: 2,
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
onFaqClick: (id) => console.log('FAQ clicked:', id),
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const WithDisabledFaq: StoryFn<ComponentProps> = Template.bind({})
|
|
76
|
+
WithDisabledFaq.args = {
|
|
77
|
+
faqs: [
|
|
78
|
+
{
|
|
79
|
+
id: '1',
|
|
80
|
+
question: 'Enabled question',
|
|
81
|
+
answer: 'Answer...',
|
|
82
|
+
enabled: true,
|
|
83
|
+
order: 1,
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: '2',
|
|
87
|
+
question: 'Disabled question',
|
|
88
|
+
answer: 'Answer...',
|
|
89
|
+
enabled: false,
|
|
90
|
+
order: 2,
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
id: '3',
|
|
94
|
+
question: 'Another enabled',
|
|
95
|
+
answer: 'Answer...',
|
|
96
|
+
enabled: true,
|
|
97
|
+
order: 3,
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
onFaqClick: (id) => console.log('FAQ clicked:', id),
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export const WithLoading: StoryFn<ComponentProps> = Template.bind({})
|
|
104
|
+
WithLoading.args = {
|
|
105
|
+
faqs: [
|
|
106
|
+
{
|
|
107
|
+
id: '1',
|
|
108
|
+
question: 'Book club slots',
|
|
109
|
+
answer: 'We ship worldwide...',
|
|
110
|
+
enabled: true,
|
|
111
|
+
order: 1,
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
id: '2',
|
|
115
|
+
question: 'Refunds',
|
|
116
|
+
answer: 'You can track...',
|
|
117
|
+
enabled: true,
|
|
118
|
+
order: 2,
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
id: '3',
|
|
122
|
+
question: 'Brand collabs',
|
|
123
|
+
answer: 'Returns are accepted...',
|
|
124
|
+
enabled: true,
|
|
125
|
+
order: 3,
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
onFaqClick: (id) => console.log('FAQ clicked:', id),
|
|
129
|
+
loadingFaqId: '2',
|
|
130
|
+
headerText: 'Tap to send a suggested question:',
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export const Empty: StoryFn<ComponentProps> = Template.bind({})
|
|
134
|
+
Empty.args = {
|
|
135
|
+
faqs: [],
|
|
136
|
+
onFaqClick: (id) => console.log('FAQ clicked:', id),
|
|
137
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Meta, StoryFn } from '@storybook/react'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
|
|
4
|
+
import { FaqListItem } from './FaqListItem'
|
|
5
|
+
type ComponentProps = React.ComponentProps<typeof FaqListItem>
|
|
6
|
+
|
|
7
|
+
const meta: Meta<ComponentProps> = {
|
|
8
|
+
title: 'FaqList/FaqListItem',
|
|
9
|
+
component: FaqListItem,
|
|
10
|
+
parameters: {
|
|
11
|
+
layout: 'centered',
|
|
12
|
+
},
|
|
13
|
+
}
|
|
14
|
+
export default meta
|
|
15
|
+
|
|
16
|
+
const Template: StoryFn<ComponentProps> = (args) => {
|
|
17
|
+
return (
|
|
18
|
+
<div className="w-96 bg-white p-4">
|
|
19
|
+
<FaqListItem {...args} />
|
|
20
|
+
</div>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const Default: StoryFn<ComponentProps> = Template.bind({})
|
|
25
|
+
Default.args = {
|
|
26
|
+
question: 'What are your shipping options?',
|
|
27
|
+
onClick: () => console.log('FAQ clicked'),
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const Loading: StoryFn<ComponentProps> = Template.bind({})
|
|
31
|
+
Loading.args = {
|
|
32
|
+
question: 'What are your shipping options?',
|
|
33
|
+
onClick: () => console.log('FAQ clicked'),
|
|
34
|
+
loading: true,
|
|
35
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import classNames from 'classnames'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
|
|
4
|
+
export interface FaqListItemProps {
|
|
5
|
+
question: string
|
|
6
|
+
onClick: () => void
|
|
7
|
+
loading?: boolean
|
|
8
|
+
className?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const FaqListItem: React.FC<FaqListItemProps> = ({
|
|
12
|
+
question,
|
|
13
|
+
onClick,
|
|
14
|
+
loading = false,
|
|
15
|
+
className,
|
|
16
|
+
}) => {
|
|
17
|
+
return (
|
|
18
|
+
<button
|
|
19
|
+
type="button"
|
|
20
|
+
onClick={onClick}
|
|
21
|
+
disabled={loading}
|
|
22
|
+
className={classNames(
|
|
23
|
+
'w-full text-center p-4 rounded-xl bg-chalk text-charcoal font-medium transition-colors',
|
|
24
|
+
{
|
|
25
|
+
'hover:bg-sand active:bg-sand': !loading,
|
|
26
|
+
'opacity-50 cursor-not-allowed': loading,
|
|
27
|
+
},
|
|
28
|
+
className
|
|
29
|
+
)}
|
|
30
|
+
>
|
|
31
|
+
{question}
|
|
32
|
+
</button>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import classNames from 'classnames'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
|
|
4
|
+
import { FaqListItem } from './FaqListItem'
|
|
5
|
+
|
|
6
|
+
export interface Faq {
|
|
7
|
+
id: string
|
|
8
|
+
question: string
|
|
9
|
+
answer: string
|
|
10
|
+
enabled: boolean
|
|
11
|
+
order?: number | null
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface FaqListProps {
|
|
15
|
+
faqs: Faq[]
|
|
16
|
+
onFaqClick: (faqId: string) => void
|
|
17
|
+
loadingFaqId?: string | null
|
|
18
|
+
headerText?: string
|
|
19
|
+
className?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const FaqList: React.FC<FaqListProps> = ({
|
|
23
|
+
faqs,
|
|
24
|
+
onFaqClick,
|
|
25
|
+
loadingFaqId,
|
|
26
|
+
headerText,
|
|
27
|
+
className,
|
|
28
|
+
}) => {
|
|
29
|
+
const enabledFaqs = faqs
|
|
30
|
+
.filter((faq) => faq.enabled)
|
|
31
|
+
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
|
|
32
|
+
|
|
33
|
+
if (enabledFaqs.length === 0) {
|
|
34
|
+
return null
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div className={classNames('px-4 py-6 space-y-3', className)}>
|
|
39
|
+
{headerText && <p className="text-md text-charcoal mb-4">{headerText}</p>}
|
|
40
|
+
{enabledFaqs.map((faq) => (
|
|
41
|
+
<FaqListItem
|
|
42
|
+
key={faq.id}
|
|
43
|
+
question={faq.question}
|
|
44
|
+
onClick={() => onFaqClick(faq.id)}
|
|
45
|
+
loading={loadingFaqId === faq.id}
|
|
46
|
+
/>
|
|
47
|
+
))}
|
|
48
|
+
</div>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
@@ -23,6 +23,7 @@ export const MessagingShell: React.FC<MessagingShellProps> = ({
|
|
|
23
23
|
onParticipantSelect,
|
|
24
24
|
initialParticipantFilter,
|
|
25
25
|
initialParticipantData,
|
|
26
|
+
CustomChannelEmptyState,
|
|
26
27
|
}) => {
|
|
27
28
|
const {
|
|
28
29
|
service,
|
|
@@ -418,6 +419,7 @@ export const MessagingShell: React.FC<MessagingShellProps> = ({
|
|
|
418
419
|
renderMessageInputActions={renderMessageInputActions}
|
|
419
420
|
onLeaveConversation={handleLeaveConversation}
|
|
420
421
|
onBlockParticipant={handleBlockParticipant}
|
|
422
|
+
CustomChannelEmptyState={CustomChannelEmptyState}
|
|
421
423
|
/>
|
|
422
424
|
</div>
|
|
423
425
|
) : (
|
package/src/index.ts
CHANGED
|
@@ -7,6 +7,8 @@ export { ChannelList } from './components/ChannelList'
|
|
|
7
7
|
export { ChannelView } from './components/ChannelView'
|
|
8
8
|
export { ParticipantPicker } from './components/ParticipantPicker'
|
|
9
9
|
export { Avatar } from './components/Avatar'
|
|
10
|
+
export { FaqList } from './components/FaqList'
|
|
11
|
+
export { FaqListItem } from './components/FaqList/FaqListItem'
|
|
10
12
|
|
|
11
13
|
// Providers
|
|
12
14
|
export { MessagingProvider } from './providers/MessagingProvider'
|
|
@@ -27,3 +29,5 @@ export type {
|
|
|
27
29
|
Participant,
|
|
28
30
|
} from './types'
|
|
29
31
|
export type { AvatarProps } from './components/Avatar'
|
|
32
|
+
export type { Faq, FaqListProps } from './components/FaqList'
|
|
33
|
+
export type { FaqListItemProps } from './components/FaqList/FaqListItem'
|
package/src/types.ts
CHANGED
|
@@ -14,7 +14,7 @@ export interface Participant {
|
|
|
14
14
|
image?: string
|
|
15
15
|
username?: string
|
|
16
16
|
phone?: string
|
|
17
|
-
metadata?: Record<string,
|
|
17
|
+
metadata?: Record<string, unknown>
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
/**
|
|
@@ -81,6 +81,12 @@ export interface MessagingShellProps {
|
|
|
81
81
|
* This reuses the existing ChannelCreator from StreamChatServiceConfig.
|
|
82
82
|
*/
|
|
83
83
|
initialParticipantData?: Participant
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Custom empty state component to render when a channel has no messages.
|
|
87
|
+
* Useful for showing FAQs or other contextual information in empty channels.
|
|
88
|
+
*/
|
|
89
|
+
CustomChannelEmptyState?: React.ComponentType
|
|
84
90
|
}
|
|
85
91
|
|
|
86
92
|
/**
|
|
@@ -106,6 +112,7 @@ export interface ChannelViewProps {
|
|
|
106
112
|
onLeaveConversation?: (channel: Channel) => void
|
|
107
113
|
onBlockParticipant?: (participantId?: string) => void
|
|
108
114
|
className?: string
|
|
115
|
+
CustomChannelEmptyState?: React.ComponentType
|
|
109
116
|
}
|
|
110
117
|
|
|
111
118
|
/**
|