@linktr.ee/messaging-react 1.6.5 → 1.7.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/dist/index.d.ts +5 -0
- package/dist/index.js +789 -649
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/ChannelList/index.tsx +53 -11
- package/src/components/ChannelView.stories.tsx +365 -0
- package/src/components/ChannelView.tsx +32 -15
- package/src/components/MessagingShell/ChannelEmptyState.tsx +17 -0
- package/src/index.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import SelectPill from '@linktr.ee/component-library/InputSelectPill'
|
|
2
|
+
import SelectPillGroup from '@linktr.ee/component-library/InputSelectPillGroup'
|
|
1
3
|
import { NotePencilIcon } from '@phosphor-icons/react'
|
|
2
4
|
import classNames from 'classnames'
|
|
3
|
-
import React from 'react'
|
|
5
|
+
import React, { useState } from 'react'
|
|
4
6
|
import {
|
|
5
7
|
ChannelList as StreamChannelList,
|
|
6
8
|
useChatContext,
|
|
@@ -12,6 +14,11 @@ import { IconButton } from '../IconButton'
|
|
|
12
14
|
|
|
13
15
|
import CustomChannelPreview from './CustomChannelPreview'
|
|
14
16
|
|
|
17
|
+
enum ChannelFilter {
|
|
18
|
+
All,
|
|
19
|
+
Unread,
|
|
20
|
+
}
|
|
21
|
+
|
|
15
22
|
/**
|
|
16
23
|
* Channel list component with customizable header and actions
|
|
17
24
|
*/
|
|
@@ -50,17 +57,27 @@ export const ChannelList: React.FC<ChannelListProps> = ({
|
|
|
50
57
|
})
|
|
51
58
|
}
|
|
52
59
|
|
|
60
|
+
const [staticFilters, setStaticFilters] = useState<{
|
|
61
|
+
type: string
|
|
62
|
+
last_message_at: { $exists: boolean }
|
|
63
|
+
has_unread: boolean | undefined
|
|
64
|
+
}>({
|
|
65
|
+
type: 'messaging',
|
|
66
|
+
last_message_at: { $exists: true },
|
|
67
|
+
has_unread: undefined,
|
|
68
|
+
})
|
|
69
|
+
|
|
53
70
|
// Filter for messaging channels
|
|
54
71
|
const filters = React.useMemo(() => {
|
|
55
72
|
const userId = client.userID
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
73
|
+
|
|
74
|
+
const newFilters = {
|
|
75
|
+
...staticFilters,
|
|
76
|
+
...(userId && {
|
|
77
|
+
members: { $in: [userId] },
|
|
78
|
+
hidden: false,
|
|
79
|
+
}),
|
|
80
|
+
}
|
|
64
81
|
|
|
65
82
|
if (debug) {
|
|
66
83
|
console.log('📺 [ChannelList] 🔍 FILTERS MEMOIZED', {
|
|
@@ -71,7 +88,7 @@ export const ChannelList: React.FC<ChannelListProps> = ({
|
|
|
71
88
|
}
|
|
72
89
|
|
|
73
90
|
return newFilters
|
|
74
|
-
}, [client.userID, debug])
|
|
91
|
+
}, [client.userID, debug, staticFilters])
|
|
75
92
|
|
|
76
93
|
return (
|
|
77
94
|
<div
|
|
@@ -83,7 +100,32 @@ export const ChannelList: React.FC<ChannelListProps> = ({
|
|
|
83
100
|
{/* Header */}
|
|
84
101
|
<div className="px-4 py-4 border-b border-sand bg-chalk">
|
|
85
102
|
<div className="flex items-center justify-between gap-3 min-h-10 min-w-0">
|
|
86
|
-
<
|
|
103
|
+
<div>
|
|
104
|
+
<h2 className="text-lg font-semibold text-charcoal">
|
|
105
|
+
Conversations
|
|
106
|
+
</h2>
|
|
107
|
+
<SelectPillGroup>
|
|
108
|
+
<SelectPill
|
|
109
|
+
checked={!filters.has_unread}
|
|
110
|
+
name="All"
|
|
111
|
+
onChange={() =>
|
|
112
|
+
setStaticFilters((state) => ({
|
|
113
|
+
...state,
|
|
114
|
+
has_unread: undefined,
|
|
115
|
+
}))
|
|
116
|
+
}
|
|
117
|
+
value={ChannelFilter.All}
|
|
118
|
+
/>
|
|
119
|
+
<SelectPill
|
|
120
|
+
checked={filters.has_unread}
|
|
121
|
+
name="Unread"
|
|
122
|
+
onChange={() =>
|
|
123
|
+
setStaticFilters((state) => ({ ...state, has_unread: true }))
|
|
124
|
+
}
|
|
125
|
+
value={ChannelFilter.Unread}
|
|
126
|
+
/>
|
|
127
|
+
</SelectPillGroup>
|
|
128
|
+
</div>
|
|
87
129
|
<div className="flex items-center gap-2">
|
|
88
130
|
{showStartConversation && onStartConversation && (
|
|
89
131
|
<IconButton
|
|
@@ -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
|
|
289
|
-
<div className="
|
|
290
|
-
<div className="flex items-center gap-
|
|
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={
|
|
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-
|
|
304
|
+
<p className="truncate text-sm text-[#00000055]">
|
|
303
305
|
{participantSecondary}
|
|
304
306
|
</p>
|
|
305
307
|
)}
|
|
306
308
|
{followerStatusLabel && (
|
|
307
|
-
<span
|
|
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
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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
|
|
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'
|