@linktr.ee/messaging-react 1.0.0 → 1.0.2
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.js +149 -140
- package/dist/index.js.map +1 -1
- package/package.json +5 -4
- package/src/components/ActionButton/ActionButton.stories.tsx +46 -0
- package/src/components/ActionButton/ActionButton.test.tsx +112 -0
- package/src/components/ActionButton/index.tsx +33 -0
- package/src/components/Avatar/Avatar.stories.tsx +144 -0
- package/src/components/Avatar/avatarColors.ts +35 -0
- package/src/components/Avatar/index.tsx +64 -0
- package/src/components/ChannelList/ChannelList.stories.tsx +48 -0
- package/src/components/ChannelList/CustomChannelPreview.stories.tsx +303 -0
- package/src/components/ChannelList/CustomChannelPreview.tsx +121 -0
- package/src/components/ChannelList/index.tsx +129 -0
- package/src/components/ChannelView.tsx +422 -0
- package/src/components/CloseButton/index.tsx +16 -0
- package/src/components/IconButton/IconButton.stories.tsx +40 -0
- package/src/components/IconButton/index.tsx +32 -0
- package/src/components/Loading/Loading.stories.tsx +24 -0
- package/src/components/Loading/index.tsx +50 -0
- package/src/components/MessagingShell/EmptyState.stories.tsx +38 -0
- package/src/components/MessagingShell/EmptyState.tsx +58 -0
- package/src/components/MessagingShell/ErrorState.stories.tsx +42 -0
- package/src/components/MessagingShell/ErrorState.tsx +33 -0
- package/src/components/MessagingShell/LoadingState.stories.tsx +26 -0
- package/src/components/MessagingShell/LoadingState.tsx +15 -0
- package/src/components/MessagingShell/index.tsx +298 -0
- package/src/components/ParticipantPicker/ParticipantItem.stories.tsx +188 -0
- package/src/components/ParticipantPicker/ParticipantItem.tsx +59 -0
- package/src/components/ParticipantPicker/ParticipantPicker.stories.tsx +54 -0
- package/src/components/ParticipantPicker/ParticipantPicker.tsx +196 -0
- package/src/components/ParticipantPicker/index.tsx +234 -0
- package/src/components/SearchInput/SearchInput.stories.tsx +33 -0
- package/src/components/SearchInput/SearchInput.test.tsx +108 -0
- package/src/components/SearchInput/index.tsx +50 -0
- package/src/hooks/useMessaging.ts +9 -0
- package/src/hooks/useParticipants.ts +92 -0
- package/src/index.ts +26 -0
- package/src/providers/MessagingProvider.tsx +282 -0
- package/src/stories/mocks.tsx +157 -0
- package/src/test/setup.ts +30 -0
- package/src/test/utils.tsx +23 -0
- package/src/types.ts +113 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import type { Meta, StoryFn } from '@storybook/react'
|
|
2
|
+
import CustomChannelPreview from './CustomChannelPreview'
|
|
3
|
+
import React from 'react'
|
|
4
|
+
|
|
5
|
+
type ComponentProps = React.ComponentProps<typeof CustomChannelPreview>
|
|
6
|
+
|
|
7
|
+
const meta: Meta<ComponentProps> = {
|
|
8
|
+
title: 'ChannelList/CustomChannelPreview',
|
|
9
|
+
component: CustomChannelPreview,
|
|
10
|
+
parameters: {
|
|
11
|
+
layout: 'centered'
|
|
12
|
+
},
|
|
13
|
+
decorators: [
|
|
14
|
+
(Story) => (
|
|
15
|
+
<Story />
|
|
16
|
+
)
|
|
17
|
+
]
|
|
18
|
+
}
|
|
19
|
+
export default meta
|
|
20
|
+
|
|
21
|
+
// Mock user for the client
|
|
22
|
+
const mockUser = {
|
|
23
|
+
id: 'current-user',
|
|
24
|
+
name: 'Current User',
|
|
25
|
+
image: 'https://i.pravatar.cc/150?img=1',
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Helper to create a mock channel
|
|
29
|
+
const createMockChannel = (options: {
|
|
30
|
+
id: string
|
|
31
|
+
participantName: string
|
|
32
|
+
participantId: string
|
|
33
|
+
participantImage?: string
|
|
34
|
+
lastMessageText?: string
|
|
35
|
+
lastMessageTime?: Date
|
|
36
|
+
unreadCount?: number
|
|
37
|
+
}): any => {
|
|
38
|
+
const {
|
|
39
|
+
id,
|
|
40
|
+
participantName,
|
|
41
|
+
participantId,
|
|
42
|
+
participantImage,
|
|
43
|
+
lastMessageText = 'Hey! How are you doing?',
|
|
44
|
+
lastMessageTime = new Date(),
|
|
45
|
+
unreadCount = 0,
|
|
46
|
+
} = options
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
id,
|
|
50
|
+
cid: `messaging:${id}`,
|
|
51
|
+
_client: {
|
|
52
|
+
userID: mockUser.id,
|
|
53
|
+
},
|
|
54
|
+
state: {
|
|
55
|
+
members: {
|
|
56
|
+
[mockUser.id]: {
|
|
57
|
+
user: mockUser,
|
|
58
|
+
user_id: mockUser.id,
|
|
59
|
+
},
|
|
60
|
+
[participantId]: {
|
|
61
|
+
user: {
|
|
62
|
+
id: participantId,
|
|
63
|
+
name: participantName,
|
|
64
|
+
image: participantImage,
|
|
65
|
+
},
|
|
66
|
+
user_id: participantId,
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
messages: lastMessageText ? [
|
|
70
|
+
{
|
|
71
|
+
id: `msg-${id}-1`,
|
|
72
|
+
text: lastMessageText,
|
|
73
|
+
created_at: lastMessageTime.toISOString(),
|
|
74
|
+
user: {
|
|
75
|
+
id: participantId,
|
|
76
|
+
name: participantName,
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
] : [],
|
|
80
|
+
unreadCount,
|
|
81
|
+
},
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const Template: StoryFn<ComponentProps> = (args) => {
|
|
86
|
+
return (
|
|
87
|
+
<div className="w-[360px]">
|
|
88
|
+
<CustomChannelPreview {...args} />
|
|
89
|
+
</div>
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export const Default: StoryFn<ComponentProps> = Template.bind({})
|
|
94
|
+
Default.args = {
|
|
95
|
+
channel: createMockChannel({
|
|
96
|
+
id: 'channel-1',
|
|
97
|
+
participantName: 'Alice Johnson',
|
|
98
|
+
participantId: 'participant-1',
|
|
99
|
+
participantImage: 'https://i.pravatar.cc/150?img=2',
|
|
100
|
+
lastMessageText: 'Hey! How are you doing?',
|
|
101
|
+
lastMessageTime: new Date(),
|
|
102
|
+
}),
|
|
103
|
+
onChannelSelect: (channel) => console.log('Channel selected:', channel.id),
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export const Selected: StoryFn<ComponentProps> = Template.bind({})
|
|
107
|
+
Selected.args = {
|
|
108
|
+
channel: createMockChannel({
|
|
109
|
+
id: 'channel-2',
|
|
110
|
+
participantName: 'Bob Smith',
|
|
111
|
+
participantId: 'participant-2',
|
|
112
|
+
participantImage: 'https://i.pravatar.cc/150?img=3',
|
|
113
|
+
lastMessageText: 'That sounds great!',
|
|
114
|
+
lastMessageTime: new Date(Date.now() - 1000 * 60 * 5), // 5 minutes ago
|
|
115
|
+
}),
|
|
116
|
+
selectedChannel: createMockChannel({
|
|
117
|
+
id: 'channel-2',
|
|
118
|
+
participantName: 'Bob Smith',
|
|
119
|
+
participantId: 'participant-2',
|
|
120
|
+
participantImage: 'https://i.pravatar.cc/150?img=3',
|
|
121
|
+
}),
|
|
122
|
+
onChannelSelect: (channel) => console.log('Channel selected:', channel.id),
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export const WithUnreadMessages: StoryFn<ComponentProps> = Template.bind({})
|
|
126
|
+
WithUnreadMessages.args = {
|
|
127
|
+
channel: createMockChannel({
|
|
128
|
+
id: 'channel-3',
|
|
129
|
+
participantName: 'Carol Williams',
|
|
130
|
+
participantId: 'participant-3',
|
|
131
|
+
participantImage: 'https://i.pravatar.cc/150?img=4',
|
|
132
|
+
lastMessageText: 'Did you see my last message?',
|
|
133
|
+
lastMessageTime: new Date(Date.now() - 1000 * 60 * 15), // 15 minutes ago
|
|
134
|
+
unreadCount: 3,
|
|
135
|
+
}),
|
|
136
|
+
onChannelSelect: (channel) => console.log('Channel selected:', channel.id),
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export const ManyUnreadMessages: StoryFn<ComponentProps> = Template.bind({})
|
|
140
|
+
ManyUnreadMessages.args = {
|
|
141
|
+
channel: createMockChannel({
|
|
142
|
+
id: 'channel-4',
|
|
143
|
+
participantName: 'David Brown',
|
|
144
|
+
participantId: 'participant-4',
|
|
145
|
+
participantImage: 'https://i.pravatar.cc/150?img=5',
|
|
146
|
+
lastMessageText: 'Please check this out!',
|
|
147
|
+
lastMessageTime: new Date(Date.now() - 1000 * 60 * 60), // 1 hour ago
|
|
148
|
+
unreadCount: 127,
|
|
149
|
+
}),
|
|
150
|
+
onChannelSelect: (channel) => console.log('Channel selected:', channel.id),
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export const NoAvatar: StoryFn<ComponentProps> = Template.bind({})
|
|
154
|
+
NoAvatar.args = {
|
|
155
|
+
channel: createMockChannel({
|
|
156
|
+
id: 'channel-5',
|
|
157
|
+
participantName: 'Emma Davis',
|
|
158
|
+
participantId: 'participant-5',
|
|
159
|
+
lastMessageText: 'Thanks for your help!',
|
|
160
|
+
lastMessageTime: new Date(Date.now() - 1000 * 60 * 30), // 30 minutes ago
|
|
161
|
+
}),
|
|
162
|
+
onChannelSelect: (channel) => console.log('Channel selected:', channel.id),
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export const NoMessages: StoryFn<ComponentProps> = Template.bind({})
|
|
166
|
+
NoMessages.args = {
|
|
167
|
+
channel: createMockChannel({
|
|
168
|
+
id: 'channel-6',
|
|
169
|
+
participantName: 'Frank Miller',
|
|
170
|
+
participantId: 'participant-6',
|
|
171
|
+
participantImage: 'https://i.pravatar.cc/150?img=6',
|
|
172
|
+
lastMessageText: '',
|
|
173
|
+
}),
|
|
174
|
+
onChannelSelect: (channel) => console.log('Channel selected:', channel.id),
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export const LongMessage: StoryFn<ComponentProps> = Template.bind({})
|
|
178
|
+
LongMessage.args = {
|
|
179
|
+
channel: createMockChannel({
|
|
180
|
+
id: 'channel-7',
|
|
181
|
+
participantName: 'Grace Lee',
|
|
182
|
+
participantId: 'participant-7',
|
|
183
|
+
participantImage: 'https://i.pravatar.cc/150?img=7',
|
|
184
|
+
lastMessageText: 'This is a very long message that should be truncated because it contains way too much text to display in the preview. We want to make sure the component handles this gracefully.',
|
|
185
|
+
lastMessageTime: new Date(Date.now() - 1000 * 60 * 60 * 2), // 2 hours ago
|
|
186
|
+
}),
|
|
187
|
+
onChannelSelect: (channel) => console.log('Channel selected:', channel.id),
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export const LongName: StoryFn<ComponentProps> = Template.bind({})
|
|
191
|
+
LongName.args = {
|
|
192
|
+
channel: createMockChannel({
|
|
193
|
+
id: 'channel-8',
|
|
194
|
+
participantName: 'Alexander Christopher Wellington-Montgomery III',
|
|
195
|
+
participantId: 'participant-8',
|
|
196
|
+
lastMessageText: 'Nice to meet you!',
|
|
197
|
+
lastMessageTime: new Date(Date.now() - 1000 * 60 * 45), // 45 minutes ago
|
|
198
|
+
}),
|
|
199
|
+
onChannelSelect: (channel) => console.log('Channel selected:', channel.id),
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export const SelectedWithUnread: StoryFn<ComponentProps> = Template.bind({})
|
|
203
|
+
SelectedWithUnread.args = {
|
|
204
|
+
channel: createMockChannel({
|
|
205
|
+
id: 'channel-9',
|
|
206
|
+
participantName: 'Helen Park',
|
|
207
|
+
participantId: 'participant-9',
|
|
208
|
+
participantImage: 'https://i.pravatar.cc/150?img=8',
|
|
209
|
+
lastMessageText: 'Important update!',
|
|
210
|
+
lastMessageTime: new Date(Date.now() - 1000 * 60 * 10), // 10 minutes ago
|
|
211
|
+
unreadCount: 5,
|
|
212
|
+
}),
|
|
213
|
+
selectedChannel: createMockChannel({
|
|
214
|
+
id: 'channel-9',
|
|
215
|
+
participantName: 'Helen Park',
|
|
216
|
+
participantId: 'participant-9',
|
|
217
|
+
}),
|
|
218
|
+
onChannelSelect: (channel) => console.log('Channel selected:', channel.id),
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export const MultipleChannels: StoryFn = () => {
|
|
222
|
+
const [selectedChannelId, setSelectedChannelId] = React.useState<string | null>('channel-2')
|
|
223
|
+
|
|
224
|
+
const channels = [
|
|
225
|
+
createMockChannel({
|
|
226
|
+
id: 'channel-1',
|
|
227
|
+
participantName: 'Alice Johnson',
|
|
228
|
+
participantId: 'participant-1',
|
|
229
|
+
participantImage: 'https://i.pravatar.cc/150?img=2',
|
|
230
|
+
lastMessageText: 'Hey! How are you doing?',
|
|
231
|
+
lastMessageTime: new Date(),
|
|
232
|
+
unreadCount: 2,
|
|
233
|
+
}),
|
|
234
|
+
createMockChannel({
|
|
235
|
+
id: 'channel-2',
|
|
236
|
+
participantName: 'Bob Smith',
|
|
237
|
+
participantId: 'participant-2',
|
|
238
|
+
participantImage: 'https://i.pravatar.cc/150?img=3',
|
|
239
|
+
lastMessageText: 'That sounds great!',
|
|
240
|
+
lastMessageTime: new Date(Date.now() - 1000 * 60 * 5), // 5 minutes ago
|
|
241
|
+
}),
|
|
242
|
+
createMockChannel({
|
|
243
|
+
id: 'channel-3',
|
|
244
|
+
participantName: 'Carol Williams',
|
|
245
|
+
participantId: 'participant-3',
|
|
246
|
+
participantImage: 'https://i.pravatar.cc/150?img=4',
|
|
247
|
+
lastMessageText: 'See you tomorrow',
|
|
248
|
+
lastMessageTime: new Date(Date.now() - 1000 * 60 * 60), // 1 hour ago
|
|
249
|
+
unreadCount: 15,
|
|
250
|
+
}),
|
|
251
|
+
createMockChannel({
|
|
252
|
+
id: 'channel-4',
|
|
253
|
+
participantName: 'David Brown',
|
|
254
|
+
participantId: 'participant-4',
|
|
255
|
+
lastMessageText: 'Thanks!',
|
|
256
|
+
lastMessageTime: new Date(Date.now() - 1000 * 60 * 60 * 2), // 2 hours ago
|
|
257
|
+
}),
|
|
258
|
+
]
|
|
259
|
+
|
|
260
|
+
const selectedChannel = channels.find(c => c.id === selectedChannelId) || null
|
|
261
|
+
|
|
262
|
+
return (
|
|
263
|
+
<div className="w-[360px] bg-chalk border border-sand rounded-lg overflow-hidden">
|
|
264
|
+
{channels.map(channel => (
|
|
265
|
+
<CustomChannelPreview
|
|
266
|
+
key={channel.id}
|
|
267
|
+
channel={channel}
|
|
268
|
+
selectedChannel={selectedChannel}
|
|
269
|
+
onChannelSelect={(channel) => {
|
|
270
|
+
console.log('Channel selected:', channel.id)
|
|
271
|
+
}}
|
|
272
|
+
/>
|
|
273
|
+
))}
|
|
274
|
+
</div>
|
|
275
|
+
)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export const WithUrl: StoryFn<ComponentProps> = Template.bind({})
|
|
279
|
+
WithUrl.args = {
|
|
280
|
+
channel: createMockChannel({
|
|
281
|
+
id: 'channel-url',
|
|
282
|
+
participantName: 'Ivan Rodriguez',
|
|
283
|
+
participantId: 'participant-url',
|
|
284
|
+
participantImage: 'https://i.pravatar.cc/150?img=10',
|
|
285
|
+
lastMessageText: 'https://example.com/page',
|
|
286
|
+
lastMessageTime: new Date(Date.now() - 1000 * 60 * 8), // 8 minutes ago
|
|
287
|
+
}),
|
|
288
|
+
onChannelSelect: (channel) => console.log('Channel selected:', channel.id),
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export const WithVeryLongUrl: StoryFn<ComponentProps> = Template.bind({})
|
|
292
|
+
WithVeryLongUrl.args = {
|
|
293
|
+
channel: createMockChannel({
|
|
294
|
+
id: 'channel-long-url',
|
|
295
|
+
participantName: 'Julia Martinez',
|
|
296
|
+
participantId: 'participant-long-url',
|
|
297
|
+
participantImage: 'https://i.pravatar.cc/150?img=11',
|
|
298
|
+
lastMessageText: 'https://example.com/very/long/path/with/many/segments/and/query/parameters?param1=value1¶m2=value2¶m3=value3¶m4=value4¶m5=very-long-value-that-makes-the-url-extremely-long',
|
|
299
|
+
lastMessageTime: new Date(Date.now() - 1000 * 60 * 20), // 20 minutes ago
|
|
300
|
+
}),
|
|
301
|
+
onChannelSelect: (channel) => console.log('Channel selected:', channel.id),
|
|
302
|
+
}
|
|
303
|
+
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { ChannelPreviewUIComponentProps } from 'stream-chat-react'
|
|
3
|
+
import { Channel } from 'stream-chat'
|
|
4
|
+
import classNames from 'classnames'
|
|
5
|
+
import { Avatar } from '../Avatar'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Custom channel preview that handles selection
|
|
9
|
+
*/
|
|
10
|
+
const CustomChannelPreview: React.FC<
|
|
11
|
+
ChannelPreviewUIComponentProps & {
|
|
12
|
+
selectedChannel?: Channel
|
|
13
|
+
onChannelSelect: (channel: Channel) => void
|
|
14
|
+
debug?: boolean
|
|
15
|
+
}
|
|
16
|
+
> = ({
|
|
17
|
+
channel,
|
|
18
|
+
selectedChannel,
|
|
19
|
+
onChannelSelect,
|
|
20
|
+
debug = false,
|
|
21
|
+
...props
|
|
22
|
+
}) => {
|
|
23
|
+
const isSelected = selectedChannel?.id === channel?.id
|
|
24
|
+
|
|
25
|
+
const handleClick = () => {
|
|
26
|
+
if (channel) {
|
|
27
|
+
onChannelSelect(channel)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Get participant info
|
|
32
|
+
const members = Object.values(channel?.state?.members || {})
|
|
33
|
+
const participant = members.find(
|
|
34
|
+
(member) => member.user?.id && member.user.id !== channel?._client?.userID
|
|
35
|
+
)
|
|
36
|
+
const participantName = participant?.user?.name || 'Conversation'
|
|
37
|
+
const participantImage = participant?.user?.image
|
|
38
|
+
const participantInitial = participantName.charAt(0).toUpperCase()
|
|
39
|
+
|
|
40
|
+
// Get last message and format timestamp
|
|
41
|
+
const lastMessage =
|
|
42
|
+
channel?.state?.messages?.[channel.state.messages.length - 1]
|
|
43
|
+
const lastMessageText = lastMessage?.text || 'No messages yet'
|
|
44
|
+
const lastMessageTime = lastMessage?.created_at
|
|
45
|
+
? new Date(lastMessage.created_at).toLocaleTimeString([], {
|
|
46
|
+
hour: '2-digit',
|
|
47
|
+
minute: '2-digit',
|
|
48
|
+
})
|
|
49
|
+
: ''
|
|
50
|
+
|
|
51
|
+
// Get unread count from channel state
|
|
52
|
+
const unread = channel?.state?.unreadCount || 0
|
|
53
|
+
|
|
54
|
+
if (debug) {
|
|
55
|
+
console.log('📺 [ChannelList] 📋 CHANNEL PREVIEW RENDER', {
|
|
56
|
+
channelId: channel?.id,
|
|
57
|
+
isSelected,
|
|
58
|
+
participantName,
|
|
59
|
+
unread,
|
|
60
|
+
hasTimestamp: !!lastMessageTime,
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<button
|
|
66
|
+
type="button"
|
|
67
|
+
onClick={handleClick}
|
|
68
|
+
className={classNames(
|
|
69
|
+
'w-full px-4 py-3 transition-colors border-b border-sand text-left max-w-full overflow-hidden focus-ring',
|
|
70
|
+
{
|
|
71
|
+
'bg-primary-alt/10 border-l-4 border-l-primary': isSelected,
|
|
72
|
+
'hover:bg-sand': !isSelected,
|
|
73
|
+
}
|
|
74
|
+
)}
|
|
75
|
+
>
|
|
76
|
+
<div className="flex items-start gap-3">
|
|
77
|
+
{/* Avatar */}
|
|
78
|
+
<Avatar
|
|
79
|
+
id={participant?.user?.id || channel.id || 'unknown'}
|
|
80
|
+
name={participantName}
|
|
81
|
+
image={participantImage}
|
|
82
|
+
size={44}
|
|
83
|
+
/>
|
|
84
|
+
|
|
85
|
+
{/* Content column */}
|
|
86
|
+
<div className="flex-1 min-w-0 flex flex-col gap-1">
|
|
87
|
+
{/* Name and timestamp row */}
|
|
88
|
+
<div className="flex items-center justify-between gap-2">
|
|
89
|
+
<h3
|
|
90
|
+
className={classNames(
|
|
91
|
+
'text-sm font-medium truncate',
|
|
92
|
+
isSelected ? 'text-primary' : 'text-charcoal'
|
|
93
|
+
)}
|
|
94
|
+
>
|
|
95
|
+
{participantName}
|
|
96
|
+
</h3>
|
|
97
|
+
{lastMessageTime && (
|
|
98
|
+
<span className="text-xs text-stone flex-shrink-0">
|
|
99
|
+
{lastMessageTime}
|
|
100
|
+
</span>
|
|
101
|
+
)}
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
{/* Message and unread badge row */}
|
|
105
|
+
<div className="flex items-center justify-between gap-2 min-w-0">
|
|
106
|
+
<p className="text-xs text-stone mr-2 flex-1 line-clamp-2">
|
|
107
|
+
{lastMessageText}
|
|
108
|
+
</p>
|
|
109
|
+
{unread > 0 && (
|
|
110
|
+
<span className="bg-primary text-white text-xs px-2 py-0.5 rounded-full min-w-[20px] text-center flex-shrink-0">
|
|
111
|
+
{unread > 99 ? '99+' : unread}
|
|
112
|
+
</span>
|
|
113
|
+
)}
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
</button>
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export default CustomChannelPreview
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { NotePencilIcon } from "@phosphor-icons/react/dist/csr/NotePencil";
|
|
3
|
+
|
|
4
|
+
import classNames from 'classnames';
|
|
5
|
+
import { ChannelList as StreamChannelList, useChatContext } from 'stream-chat-react';
|
|
6
|
+
import type { ChannelListProps } from '../../types';
|
|
7
|
+
import { IconButton } from '../IconButton';
|
|
8
|
+
import { useMessagingContext } from '../../providers/MessagingProvider';
|
|
9
|
+
import CustomChannelPreview from './CustomChannelPreview';
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Channel list component with customizable header and actions
|
|
14
|
+
*/
|
|
15
|
+
export const ChannelList: React.FC<ChannelListProps> = ({
|
|
16
|
+
onChannelSelect,
|
|
17
|
+
selectedChannel,
|
|
18
|
+
showStartConversation = false,
|
|
19
|
+
onStartConversation,
|
|
20
|
+
participantLabel = 'participants',
|
|
21
|
+
className,
|
|
22
|
+
}) => {
|
|
23
|
+
// Track renders
|
|
24
|
+
const renderCountRef = React.useRef(0);
|
|
25
|
+
renderCountRef.current++;
|
|
26
|
+
|
|
27
|
+
// Get debug flag from context
|
|
28
|
+
const { debug = false } = useMessagingContext();
|
|
29
|
+
|
|
30
|
+
if (debug) {
|
|
31
|
+
console.log('📺 [ChannelList] 🔄 RENDER START', {
|
|
32
|
+
renderCount: renderCountRef.current,
|
|
33
|
+
selectedChannelId: selectedChannel?.id,
|
|
34
|
+
showStartConversation,
|
|
35
|
+
participantLabel
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const { client } = useChatContext();
|
|
40
|
+
|
|
41
|
+
if (debug) {
|
|
42
|
+
console.log('📺 [ChannelList] 📡 CHAT CONTEXT', {
|
|
43
|
+
renderCount: renderCountRef.current,
|
|
44
|
+
hasClient: !!client,
|
|
45
|
+
clientUserId: client?.userID,
|
|
46
|
+
clientConnected: client?.wsConnection?.isHealthy
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Filter for messaging channels
|
|
51
|
+
const filters = React.useMemo(() => {
|
|
52
|
+
const userId = client.userID;
|
|
53
|
+
const newFilters = userId
|
|
54
|
+
? { type: 'messaging', members: { $in: [userId] }, hidden: false }
|
|
55
|
+
: { type: 'messaging' };
|
|
56
|
+
|
|
57
|
+
if (debug) {
|
|
58
|
+
console.log('📺 [ChannelList] 🔍 FILTERS MEMOIZED', {
|
|
59
|
+
renderCount: renderCountRef.current,
|
|
60
|
+
userId,
|
|
61
|
+
filters: newFilters
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return newFilters;
|
|
66
|
+
}, [client.userID, debug]);
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<div className={classNames('h-full flex flex-col min-w-0 overflow-hidden', className)}>
|
|
70
|
+
{/* Header */}
|
|
71
|
+
<div className="px-4 py-4 border-b border-sand bg-chalk">
|
|
72
|
+
<div className="flex items-center justify-between gap-3 min-h-10 min-w-0">
|
|
73
|
+
<h2 className="text-lg font-semibold text-charcoal">Conversations</h2>
|
|
74
|
+
<div className="flex items-center gap-2">
|
|
75
|
+
{showStartConversation && onStartConversation && (
|
|
76
|
+
<IconButton
|
|
77
|
+
label="Start a new conversation"
|
|
78
|
+
onClick={onStartConversation}
|
|
79
|
+
className="inline-flex size-10 items-center justify-center"
|
|
80
|
+
>
|
|
81
|
+
<NotePencilIcon className="h-5 w-5" />
|
|
82
|
+
</IconButton>
|
|
83
|
+
)}
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
{/* Channel List */}
|
|
89
|
+
<div className="flex-1 overflow-hidden min-w-0">
|
|
90
|
+
{(() => {
|
|
91
|
+
if (debug) {
|
|
92
|
+
console.log('📺 [ChannelList] 🎬 RENDERING STREAM CHANNEL LIST', {
|
|
93
|
+
renderCount: renderCountRef.current,
|
|
94
|
+
filters,
|
|
95
|
+
hasClient: !!client,
|
|
96
|
+
clientUserId: client?.userID
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<StreamChannelList
|
|
102
|
+
filters={filters}
|
|
103
|
+
sort={{ last_message_at: -1 }}
|
|
104
|
+
options={{ limit: 30 }}
|
|
105
|
+
Preview={(props) => {
|
|
106
|
+
if (debug) {
|
|
107
|
+
console.log('📺 [ChannelList] 📋 CHANNEL PREVIEW RENDER', {
|
|
108
|
+
channelId: props.channel?.id,
|
|
109
|
+
selectedChannelId: selectedChannel?.id,
|
|
110
|
+
isSelected: selectedChannel?.id === props.channel?.id
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<CustomChannelPreview
|
|
116
|
+
{...props}
|
|
117
|
+
selectedChannel={selectedChannel}
|
|
118
|
+
onChannelSelect={onChannelSelect}
|
|
119
|
+
debug={debug}
|
|
120
|
+
/>
|
|
121
|
+
);
|
|
122
|
+
}}
|
|
123
|
+
/>
|
|
124
|
+
);
|
|
125
|
+
})()}
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
);
|
|
129
|
+
};
|