@linktr.ee/messaging-react 1.6.5 → 1.6.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linktr.ee/messaging-react",
3
- "version": "1.6.5",
3
+ "version": "1.6.6",
4
4
  "description": "React messaging components built on messaging-core for web applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -0,0 +1,365 @@
1
+ import type { Meta, StoryFn } from '@storybook/react'
2
+ import React, { useEffect } from 'react'
3
+ import { Channel as ChannelType, QueryChannelAPIResponse, StreamChat } from 'stream-chat'
4
+ import { Chat } from 'stream-chat-react'
5
+
6
+ import { mockParticipants } from '../stories/mocks'
7
+
8
+ import { ChannelView } from './ChannelView'
9
+ import { ChannelEmptyState } from './MessagingShell/ChannelEmptyState'
10
+
11
+ type ComponentProps = React.ComponentProps<typeof ChannelView>
12
+
13
+ const meta: Meta<ComponentProps> = {
14
+ title: 'ChannelView',
15
+ component: ChannelView,
16
+ parameters: {
17
+ layout: 'fullscreen',
18
+ viewport: {
19
+ defaultViewport: 'responsive',
20
+ },
21
+ },
22
+ }
23
+ export default meta
24
+
25
+ // Mock user for Storybook
26
+ const mockUser = {
27
+ id: 'storybook-user',
28
+ name: 'Storybook User',
29
+ image: 'https://i.pravatar.cc/150?img=1',
30
+ }
31
+
32
+ // Create a real channel using client.channel() and mock the API responses
33
+ const createMockChannel = async (
34
+ client: StreamChat,
35
+ hasMessages = true,
36
+ followerStatus?: string | boolean
37
+ ) => {
38
+ const participant = mockParticipants[0]
39
+
40
+ const mockMessages = hasMessages
41
+ ? [
42
+ {
43
+ id: 'msg-1',
44
+ text: 'Hey! How are you doing?',
45
+ type: 'regular' as const,
46
+ created_at: new Date(Date.now() - 1000 * 60 * 60),
47
+ updated_at: new Date(Date.now() - 1000 * 60 * 60),
48
+ user: participant,
49
+ html: '<p>Hey! How are you doing?</p>',
50
+ attachments: [],
51
+ latest_reactions: [],
52
+ own_reactions: [],
53
+ reaction_counts: {},
54
+ reaction_scores: {},
55
+ reply_count: 0,
56
+ status: 'received',
57
+ cid: 'messaging:storybook-channel-1',
58
+ mentioned_users: [],
59
+ },
60
+ {
61
+ id: 'msg-2',
62
+ text: "I'm doing great, thanks! How about you?",
63
+ type: 'regular' as const,
64
+ created_at: new Date(Date.now() - 1000 * 60 * 50),
65
+ updated_at: new Date(Date.now() - 1000 * 60 * 50),
66
+ user: mockUser,
67
+ html: "<p>I'm doing great, thanks! How about you?</p>",
68
+ attachments: [],
69
+ latest_reactions: [],
70
+ own_reactions: [],
71
+ reaction_counts: {},
72
+ reaction_scores: {},
73
+ reply_count: 0,
74
+ status: 'received',
75
+ cid: 'messaging:storybook-channel-1',
76
+ mentioned_users: [],
77
+ },
78
+ {
79
+ id: 'msg-3',
80
+ text: 'Pretty good! Just working on some exciting stuff.',
81
+ type: 'regular' as const,
82
+ created_at: new Date(Date.now() - 1000 * 60 * 30),
83
+ updated_at: new Date(Date.now() - 1000 * 60 * 30),
84
+ user: participant,
85
+ html: '<p>Pretty good! Just working on some exciting stuff.</p>',
86
+ attachments: [],
87
+ latest_reactions: [],
88
+ own_reactions: [],
89
+ reaction_counts: {},
90
+ reaction_scores: {},
91
+ reply_count: 0,
92
+ status: 'received',
93
+ cid: 'messaging:storybook-channel-1',
94
+ mentioned_users: [],
95
+ },
96
+ ]
97
+ : []
98
+
99
+ // Prepare channel data with optional follower status
100
+ const channelData: Record<string, unknown> = {
101
+ members: [mockUser.id, participant.id],
102
+ }
103
+
104
+ // Add follower status if provided
105
+ if (typeof followerStatus === 'string') {
106
+ channelData.followerStatus = followerStatus
107
+ } else if (typeof followerStatus === 'boolean') {
108
+ channelData.isFollower = followerStatus
109
+ }
110
+
111
+ // Create a real channel using the client
112
+ const channel = client.channel('messaging', 'storybook-channel-1', channelData)
113
+
114
+ // Mock the watch method to return mocked data
115
+ channel.watch = async () => {
116
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
117
+ channel.state.messages = mockMessages as unknown as any[]
118
+ channel.state.members = {
119
+ [mockUser.id]: {
120
+ user: mockUser,
121
+ user_id: mockUser.id,
122
+ },
123
+ [participant.id]: {
124
+ user: participant,
125
+ user_id: participant.id,
126
+ },
127
+ }
128
+ return {
129
+ channel: channelData,
130
+ members: [],
131
+ messages: mockMessages,
132
+ watchers: [],
133
+ pinned_messages: [],
134
+ duration: '0ms',
135
+ } as unknown as QueryChannelAPIResponse
136
+ }
137
+
138
+ // Initialize the channel
139
+ try {
140
+ await channel.watch()
141
+ } catch (e) {
142
+ // Ignore errors - we're in mock mode
143
+ }
144
+
145
+ // Force set the channel data after watch
146
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
147
+ ;(channel as any)._data = channelData
148
+
149
+ return channel
150
+ }
151
+
152
+ type TemplateProps = ComponentProps & { followerStatus?: string | boolean }
153
+
154
+ const Template: StoryFn<TemplateProps> = (args) => {
155
+ const { followerStatus, ...channelViewProps } = args
156
+ const [client] = React.useState(() => {
157
+ const client = new StreamChat('mock-api-key', {
158
+ allowServerSideConnect: true,
159
+ })
160
+ client.userID = mockUser.id
161
+ client.user = mockUser
162
+ return client
163
+ })
164
+
165
+ const [channel, setChannel] = React.useState<ChannelType | null>(null)
166
+
167
+ useEffect(() => {
168
+ createMockChannel(client, true, followerStatus).then((mockChannel) => {
169
+ setChannel(mockChannel)
170
+ })
171
+ }, [client, followerStatus])
172
+
173
+ if (!channel) {
174
+ return <div>Loading...</div>
175
+ }
176
+
177
+ return (
178
+ <Chat client={client}>
179
+ <div className="h-screen w-full bg-white">
180
+ <ChannelView {...channelViewProps} channel={channel} />
181
+ </div>
182
+ </Chat>
183
+ )
184
+ }
185
+
186
+ export const Default: StoryFn<TemplateProps> = Template.bind({})
187
+ Default.args = {
188
+ showBackButton: false,
189
+ onBack: () => console.log('Back clicked'),
190
+ onLeaveConversation: (channel) =>
191
+ console.log('Leave conversation:', channel.id),
192
+ onBlockParticipant: (participantId) =>
193
+ console.log('Block participant:', participantId),
194
+ followerStatus: true, // Shows "Subscribed to you"
195
+ }
196
+ Default.parameters = {
197
+ docs: {
198
+ description: {
199
+ story: 'Default channel view with messages and conversation header, showing subscriber status.',
200
+ },
201
+ },
202
+ }
203
+
204
+ export const WithBackButton: StoryFn<TemplateProps> = Template.bind({})
205
+ WithBackButton.args = {
206
+ showBackButton: true,
207
+ onBack: () => console.log('Back clicked'),
208
+ onLeaveConversation: (channel) =>
209
+ console.log('Leave conversation:', channel.id),
210
+ onBlockParticipant: (participantId) =>
211
+ console.log('Block participant:', participantId),
212
+ }
213
+ WithBackButton.parameters = {
214
+ viewport: {
215
+ defaultViewport: 'mobile1',
216
+ },
217
+ docs: {
218
+ description: {
219
+ story:
220
+ 'Channel view with back button visible on mobile (switch to mobile viewport to see the back button).',
221
+ },
222
+ },
223
+ }
224
+
225
+ export const WithMessageActions: StoryFn<TemplateProps> = Template.bind({})
226
+ WithMessageActions.args = {
227
+ showBackButton: true,
228
+ onBack: () => console.log('Back clicked'),
229
+ renderMessageInputActions: (channel) => (
230
+ <button
231
+ onClick={() => console.log('Custom action clicked', channel.id)}
232
+ className="p-2 hover:bg-sand rounded-lg"
233
+ aria-label="Attach file"
234
+ >
235
+ 📎
236
+ </button>
237
+ ),
238
+ }
239
+ WithMessageActions.parameters = {
240
+ viewport: {
241
+ defaultViewport: 'mobile1',
242
+ },
243
+ docs: {
244
+ description: {
245
+ story:
246
+ 'Channel view with custom action buttons in the message input area (e.g., attachment button).',
247
+ },
248
+ },
249
+ }
250
+
251
+ const EmptyTemplate: StoryFn<ComponentProps> = (args) => {
252
+ const [client] = React.useState(() => {
253
+ const client = new StreamChat('mock-api-key', {
254
+ allowServerSideConnect: true,
255
+ })
256
+ client.userID = mockUser.id
257
+ client.user = mockUser
258
+ return client
259
+ })
260
+
261
+ const [channel, setChannel] = React.useState<ChannelType | null>(null)
262
+
263
+ useEffect(() => {
264
+ createMockChannel(client, false).then((mockChannel) => {
265
+ setChannel(mockChannel)
266
+ })
267
+ }, [client])
268
+
269
+ if (!channel) {
270
+ return <div>Loading...</div>
271
+ }
272
+
273
+ return (
274
+ <Chat client={client}>
275
+ <div className="h-screen w-full bg-white">
276
+ <ChannelView {...args} channel={channel} />
277
+ </div>
278
+ </Chat>
279
+ )
280
+ }
281
+
282
+ export const EmptyChannel: StoryFn<ComponentProps> = EmptyTemplate.bind({})
283
+ EmptyChannel.args = {
284
+ showBackButton: true,
285
+ CustomChannelEmptyState: ChannelEmptyState,
286
+ }
287
+ EmptyChannel.parameters = {
288
+ viewport: {
289
+ defaultViewport: 'mobile1',
290
+ },
291
+ docs: {
292
+ description: {
293
+ story:
294
+ 'Channel view with no messages showing a custom empty state component.',
295
+ },
296
+ },
297
+ }
298
+
299
+ export const SubscriberStatus: StoryFn<TemplateProps> = Template.bind({})
300
+ SubscriberStatus.args = {
301
+ showBackButton: false,
302
+ followerStatus: true, // Shows "Subscribed to you" in green
303
+ onLeaveConversation: (channel) =>
304
+ console.log('Leave conversation:', channel.id),
305
+ onBlockParticipant: (participantId) =>
306
+ console.log('Block participant:', participantId),
307
+ }
308
+ SubscriberStatus.parameters = {
309
+ docs: {
310
+ description: {
311
+ story: 'Channel view showing "Subscribed to you" badge in green when isFollower is true.',
312
+ },
313
+ },
314
+ }
315
+
316
+ export const NotSubscribedStatus: StoryFn<TemplateProps> = Template.bind({})
317
+ NotSubscribedStatus.args = {
318
+ showBackButton: false,
319
+ followerStatus: false, // Shows "Not subscribed" in gray
320
+ onLeaveConversation: (channel) =>
321
+ console.log('Leave conversation:', channel.id),
322
+ onBlockParticipant: (participantId) =>
323
+ console.log('Block participant:', participantId),
324
+ }
325
+ NotSubscribedStatus.parameters = {
326
+ docs: {
327
+ description: {
328
+ story: 'Channel view showing "Not subscribed" badge in gray when isFollower is false.',
329
+ },
330
+ },
331
+ }
332
+
333
+ export const CustomFollowerStatus: StoryFn<TemplateProps> = Template.bind({})
334
+ CustomFollowerStatus.args = {
335
+ showBackButton: false,
336
+ followerStatus: 'Mutual subscribers', // Custom status text in gray
337
+ onLeaveConversation: (channel) =>
338
+ console.log('Leave conversation:', channel.id),
339
+ onBlockParticipant: (participantId) =>
340
+ console.log('Block participant:', participantId),
341
+ }
342
+ CustomFollowerStatus.parameters = {
343
+ docs: {
344
+ description: {
345
+ story: 'Channel view with a custom follower status text (shows in gray unless text is "Subscribed to you").',
346
+ },
347
+ },
348
+ }
349
+
350
+ export const NoFollowerStatus: StoryFn<TemplateProps> = Template.bind({})
351
+ NoFollowerStatus.args = {
352
+ showBackButton: false,
353
+ followerStatus: undefined, // No badge shown
354
+ onLeaveConversation: (channel) =>
355
+ console.log('Leave conversation:', channel.id),
356
+ onBlockParticipant: (participantId) =>
357
+ console.log('Block participant:', participantId),
358
+ }
359
+ NoFollowerStatus.parameters = {
360
+ docs: {
361
+ description: {
362
+ story: 'Channel view with no follower status badge displayed.',
363
+ },
364
+ },
365
+ }
@@ -24,6 +24,7 @@ import ActionButton from './ActionButton'
24
24
  import { Avatar } from './Avatar'
25
25
  import { CloseButton } from './CloseButton'
26
26
  import { IconButton } from './IconButton'
27
+ import { ChannelEmptyState } from './MessagingShell/ChannelEmptyState'
27
28
 
28
29
  // Custom user type with email and username
29
30
  type CustomUser = {
@@ -285,26 +286,36 @@ const ChannelInfoDialog: React.FC<{
285
286
  <CloseButton onClick={onClose} />
286
287
  </div>
287
288
 
288
- <div className="flex-1 overflow-y-auto px-6 py-6">
289
- <div className="rounded-2xl bg-chalk p-4">
290
- <div className="flex items-center gap-4">
289
+ <div className="flex-1 px-2 overflow-y-auto w-full">
290
+ <div className="flex flex-col items-center gap-3 self-stretch px-4 py-2 mt-6 rounded-lg border border-black/[0.04]" style={{ backgroundColor: '#FBFAF9' }}>
291
+ <div className="flex items-center gap-3 w-full">
291
292
  <Avatar
292
293
  id={participantId}
293
294
  name={participantName}
294
295
  image={participantImage}
295
- size={64}
296
+ size={88}
297
+ className="!rounded-full"
296
298
  />
297
- <div className="min-w-0 flex-1">
299
+ <div className="flex flex-col min-w-0 flex-1">
298
300
  <p className="truncate text-base font-semibold text-charcoal">
299
301
  {participantName}
300
302
  </p>
301
303
  {participantSecondary && (
302
- <p className="truncate text-sm text-stone">
304
+ <p className="truncate text-sm text-[#00000055]">
303
305
  {participantSecondary}
304
306
  </p>
305
307
  )}
306
308
  {followerStatusLabel && (
307
- <span className="mt-2 inline-flex items-center rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-700">
309
+ <span
310
+ className="mt-1 rounded-full text-xs font-normal w-fit"
311
+ style={{
312
+ padding: '4px 8px',
313
+ backgroundColor: followerStatusLabel === 'Subscribed to you' ? '#DCFCE7' : '#F5F5F4',
314
+ color: followerStatusLabel === 'Subscribed to you' ? '#008236' : '#78716C',
315
+ lineHeight: '133.333%',
316
+ letterSpacing: '0.21px',
317
+ }}
318
+ >
308
319
  {followerStatusLabel}
309
320
  </span>
310
321
  )}
@@ -385,7 +396,7 @@ const ChannelViewInner: React.FC<{
385
396
  renderMessageInputActions,
386
397
  onLeaveConversation,
387
398
  onBlockParticipant,
388
- CustomChannelEmptyState,
399
+ CustomChannelEmptyState = ChannelEmptyState,
389
400
  }) => {
390
401
  const { channel } = useChannelStateContext()
391
402
  const [showInfo, setShowInfo] = useState(false)
@@ -407,11 +418,17 @@ const ChannelViewInner: React.FC<{
407
418
  followerStatus?: string
408
419
  isFollower?: boolean
409
420
  }
410
- return channelExtraData.followerStatus
411
- ? String(channelExtraData.followerStatus)
412
- : channelExtraData.isFollower
413
- ? 'Subscribed to you'
414
- : undefined
421
+
422
+ // If explicit followerStatus is provided, use it
423
+ if (channelExtraData.followerStatus) {
424
+ return String(channelExtraData.followerStatus)
425
+ }
426
+ // If isFollower is explicitly defined, use it to determine status
427
+ if (channelExtraData.isFollower !== undefined) {
428
+ return channelExtraData.isFollower ? 'Subscribed to you' : 'Not subscribed'
429
+ }
430
+ // Otherwise, don't show any status
431
+ return undefined
415
432
  }, [channel.data])
416
433
 
417
434
  return (
@@ -433,7 +450,7 @@ const ChannelViewInner: React.FC<{
433
450
 
434
451
  {/* Show custom empty state when no messages */}
435
452
  {!hasMessages && CustomChannelEmptyState && (
436
- <div className="absolute inset-0 flex bg-white">
453
+ <div className="absolute inset-0 w-full h-full bg-white">
437
454
  <CustomChannelEmptyState />
438
455
  </div>
439
456
  )}
@@ -470,7 +487,7 @@ export const ChannelView: React.FC<ChannelViewProps> = ({
470
487
  onLeaveConversation,
471
488
  onBlockParticipant,
472
489
  className,
473
- CustomChannelEmptyState,
490
+ CustomChannelEmptyState = ChannelEmptyState,
474
491
  }) => {
475
492
  return (
476
493
  <div
@@ -0,0 +1,17 @@
1
+ import React from 'react'
2
+
3
+ /**
4
+ * Empty state component shown when a channel has no messages
5
+ */
6
+ export const ChannelEmptyState: React.FC = () => (
7
+ <div className="messaging-channel-empty-state flex items-center justify-center h-full p-8 text-balance">
8
+ <div className="text-center max-w-sm">
9
+ <h2 className="font-semibold text-charcoal mb-2">No messages yet 👀</h2>
10
+
11
+ <p className="text-stone text-xs">
12
+ Share to social media to generate more conversations
13
+ </p>
14
+ </div>
15
+ </div>
16
+ )
17
+
package/src/index.ts CHANGED
@@ -9,6 +9,7 @@ export { ParticipantPicker } from './components/ParticipantPicker'
9
9
  export { Avatar } from './components/Avatar'
10
10
  export { FaqList } from './components/FaqList'
11
11
  export { FaqListItem } from './components/FaqList/FaqListItem'
12
+ export { ChannelEmptyState } from './components/MessagingShell/ChannelEmptyState'
12
13
 
13
14
  // Providers
14
15
  export { MessagingProvider } from './providers/MessagingProvider'