@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.
@@ -370,19 +370,29 @@ const ChannelInfoDialog: React.FC<{
370
370
  }
371
371
 
372
372
  /**
373
- * Channel view component with message list and input
373
+ * Inner component that has access to channel context
374
374
  */
375
- export const ChannelView: React.FC<ChannelViewProps> = ({
376
- channel,
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 = false,
384
+ showBackButton,
379
385
  renderMessageInputActions,
380
386
  onLeaveConversation,
381
387
  onBlockParticipant,
382
- className,
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
- <div
409
- className={classNames(
410
- 'messaging-channel-view h-full flex flex-col',
411
- className
412
- )}
413
- >
414
- <Channel channel={channel}>
415
- <Window>
416
- {/* Custom Channel Header */}
417
- <div className="border-b border-sand bg-white px-4 py-3">
418
- <CustomChannelHeader
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
- {/* Message List */}
427
- <div className="flex-1 overflow-hidden">
428
- <MessageList hideDeletedMessages hideNewMessageSeparator={false} />
429
- </div>
430
+ {/* Message List */}
431
+ <div className="flex-1 overflow-hidden">
432
+ <MessageList hideDeletedMessages hideNewMessageSeparator={false} />
430
433
 
431
- {/* Message Input */}
432
- <CustomMessageInput
433
- renderActions={() => renderMessageInputActions?.(channel)}
434
- />
435
- </Window>
436
- </Channel>
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, any>
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
  /**