@linktr.ee/messaging-react 1.0.2 → 1.1.0-rc-1760927977
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 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +836 -1079
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
- package/src/components/ActionButton/ActionButton.stories.tsx +2 -1
- package/src/components/ActionButton/ActionButton.test.tsx +2 -0
- package/src/components/Avatar/Avatar.stories.tsx +2 -1
- package/src/components/Avatar/index.tsx +2 -1
- package/src/components/ChannelList/ChannelList.stories.tsx +4 -2
- package/src/components/ChannelList/CustomChannelPreview.stories.tsx +31 -27
- package/src/components/ChannelList/CustomChannelPreview.tsx +5 -11
- package/src/components/ChannelList/index.tsx +43 -35
- package/src/components/ChannelView.tsx +150 -127
- package/src/components/CloseButton/index.tsx +4 -5
- package/src/components/IconButton/IconButton.stories.tsx +3 -3
- package/src/components/Loading/Loading.stories.tsx +2 -1
- package/src/components/Loading/index.tsx +7 -9
- package/src/components/MessagingShell/EmptyState.stories.tsx +2 -1
- package/src/components/MessagingShell/ErrorState.stories.tsx +2 -1
- package/src/components/MessagingShell/LoadingState.stories.tsx +2 -1
- package/src/components/MessagingShell/LoadingState.tsx +3 -5
- package/src/components/MessagingShell/index.tsx +159 -135
- package/src/components/ParticipantPicker/ParticipantItem.stories.tsx +4 -2
- package/src/components/ParticipantPicker/ParticipantItem.tsx +25 -21
- package/src/components/ParticipantPicker/ParticipantPicker.stories.tsx +4 -2
- package/src/components/ParticipantPicker/ParticipantPicker.tsx +104 -76
- package/src/components/ParticipantPicker/index.tsx +93 -72
- package/src/components/SearchInput/SearchInput.stories.tsx +2 -1
- package/src/components/SearchInput/SearchInput.test.tsx +4 -2
- package/src/components/SearchInput/index.tsx +14 -15
- package/src/hooks/useParticipants.ts +1 -0
- package/src/index.ts +3 -0
- package/src/providers/MessagingProvider.tsx +213 -135
- package/src/stories/mocks.tsx +18 -19
- package/src/styles.css +75 -0
- package/src/test/setup.ts +11 -12
- package/src/test/utils.tsx +6 -7
- package/src/types.ts +1 -1
|
@@ -1,65 +1,68 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import type { Channel } from 'stream-chat'
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
import {
|
|
1
|
+
import classNames from 'classnames'
|
|
2
|
+
import React, { useState, useCallback, useRef, useEffect } from 'react'
|
|
3
|
+
import type { Channel } from 'stream-chat'
|
|
4
|
+
|
|
5
|
+
import { useMessaging } from '../../hooks/useMessaging'
|
|
6
|
+
import type { MessagingShellProps, Participant } from '../../types'
|
|
7
|
+
import { ChannelList } from '../ChannelList'
|
|
8
|
+
import { ChannelView } from '../ChannelView'
|
|
9
|
+
import { ParticipantPicker } from '../ParticipantPicker'
|
|
10
|
+
|
|
11
|
+
import { EmptyState } from './EmptyState'
|
|
12
|
+
import { ErrorState } from './ErrorState'
|
|
13
|
+
import { LoadingState } from './LoadingState'
|
|
12
14
|
|
|
13
15
|
/**
|
|
14
16
|
* Main messaging interface component that combines channel list and channel view
|
|
15
17
|
*/
|
|
16
18
|
export const MessagingShell: React.FC<MessagingShellProps> = ({
|
|
17
19
|
capabilities = {},
|
|
18
|
-
customization = {},
|
|
19
20
|
className,
|
|
20
21
|
renderMessageInputActions,
|
|
21
22
|
onChannelSelect,
|
|
22
23
|
onParticipantSelect,
|
|
23
24
|
}) => {
|
|
24
|
-
const {
|
|
25
|
-
service,
|
|
26
|
-
client,
|
|
27
|
-
isConnected,
|
|
28
|
-
isLoading,
|
|
29
|
-
error,
|
|
25
|
+
const {
|
|
26
|
+
service,
|
|
27
|
+
client,
|
|
28
|
+
isConnected,
|
|
29
|
+
isLoading,
|
|
30
|
+
error,
|
|
30
31
|
refreshConnection,
|
|
31
32
|
debug,
|
|
32
|
-
} = useMessaging()
|
|
33
|
+
} = useMessaging()
|
|
34
|
+
|
|
35
|
+
const [selectedChannel, setSelectedChannel] = useState<Channel | null>(null)
|
|
36
|
+
const [hasChannels, setHasChannels] = useState(false)
|
|
37
|
+
const [_showParticipantPicker, setShowParticipantPicker] = useState(false)
|
|
38
|
+
const [existingParticipantIds, setExistingParticipantIds] = useState<
|
|
39
|
+
Set<string>
|
|
40
|
+
>(new Set())
|
|
41
|
+
const [pickerKey, setPickerKey] = useState(0) // Key to force remount of ParticipantPicker
|
|
33
42
|
|
|
34
|
-
const
|
|
35
|
-
const [hasChannels, setHasChannels] = useState(false);
|
|
36
|
-
const [showParticipantPicker, setShowParticipantPicker] = useState(false);
|
|
37
|
-
const [existingParticipantIds, setExistingParticipantIds] = useState<Set<string>>(new Set());
|
|
38
|
-
const [pickerKey, setPickerKey] = useState(0); // Key to force remount of ParticipantPicker
|
|
43
|
+
const participantPickerRef = useRef<HTMLDialogElement>(null)
|
|
39
44
|
|
|
40
|
-
const participantPickerRef = useRef<HTMLDialogElement>(null);
|
|
41
|
-
|
|
42
45
|
const {
|
|
43
46
|
showStartConversation = false,
|
|
44
47
|
participantSource,
|
|
45
48
|
participantLabel = 'participants',
|
|
46
|
-
} = capabilities
|
|
49
|
+
} = capabilities
|
|
47
50
|
|
|
48
51
|
// Track if we've already synced channels to prevent repeated API calls
|
|
49
|
-
const syncedRef = useRef<string | null>(null)
|
|
50
|
-
|
|
52
|
+
const syncedRef = useRef<string | null>(null)
|
|
53
|
+
|
|
51
54
|
// Function to sync channels (extracted for reuse)
|
|
52
55
|
const syncChannels = useCallback(async () => {
|
|
53
|
-
if (!client || !isConnected) return
|
|
54
|
-
|
|
55
|
-
const userId = client.userID
|
|
56
|
-
if (!userId) return
|
|
56
|
+
if (!client || !isConnected) return
|
|
57
|
+
|
|
58
|
+
const userId = client.userID
|
|
59
|
+
if (!userId) return
|
|
57
60
|
|
|
58
61
|
try {
|
|
59
62
|
if (debug) {
|
|
60
|
-
console.log('[MessagingShell] Syncing channels for user:', userId)
|
|
63
|
+
console.log('[MessagingShell] Syncing channels for user:', userId)
|
|
61
64
|
}
|
|
62
|
-
|
|
65
|
+
|
|
63
66
|
const channels = await client.queryChannels(
|
|
64
67
|
{
|
|
65
68
|
type: 'messaging',
|
|
@@ -67,125 +70,140 @@ export const MessagingShell: React.FC<MessagingShellProps> = ({
|
|
|
67
70
|
},
|
|
68
71
|
{},
|
|
69
72
|
{ limit: 100 }
|
|
70
|
-
)
|
|
73
|
+
)
|
|
71
74
|
|
|
72
|
-
const memberIds = new Set<string>()
|
|
75
|
+
const memberIds = new Set<string>()
|
|
73
76
|
channels.forEach((channel: Channel) => {
|
|
74
|
-
const members = channel.state.members
|
|
75
|
-
Object.values(members).forEach((member
|
|
76
|
-
const memberId = member.user?.id
|
|
77
|
+
const members = channel.state.members
|
|
78
|
+
Object.values(members).forEach((member) => {
|
|
79
|
+
const memberId = member.user?.id
|
|
77
80
|
if (memberId && memberId !== userId) {
|
|
78
|
-
memberIds.add(memberId)
|
|
81
|
+
memberIds.add(memberId)
|
|
79
82
|
}
|
|
80
|
-
})
|
|
81
|
-
})
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
setExistingParticipantIds(memberIds)
|
|
87
|
+
setHasChannels(channels.length > 0)
|
|
88
|
+
syncedRef.current = userId // Mark as synced for this user
|
|
82
89
|
|
|
83
|
-
setExistingParticipantIds(memberIds);
|
|
84
|
-
setHasChannels(channels.length > 0);
|
|
85
|
-
syncedRef.current = userId; // Mark as synced for this user
|
|
86
|
-
|
|
87
90
|
if (debug) {
|
|
88
91
|
console.log('[MessagingShell] Channels synced successfully:', {
|
|
89
92
|
channelCount: channels.length,
|
|
90
|
-
memberCount: memberIds.size
|
|
91
|
-
})
|
|
93
|
+
memberCount: memberIds.size,
|
|
94
|
+
})
|
|
92
95
|
}
|
|
93
96
|
} catch (error) {
|
|
94
|
-
console.error('[MessagingShell] Failed to sync channels:', error)
|
|
97
|
+
console.error('[MessagingShell] Failed to sync channels:', error)
|
|
95
98
|
// Don't mark as synced on error, allow retry
|
|
96
99
|
}
|
|
97
|
-
}, [client, isConnected, debug])
|
|
98
|
-
|
|
100
|
+
}, [client, isConnected, debug])
|
|
101
|
+
|
|
99
102
|
// Sync existing channels to track which participants we can already message
|
|
100
103
|
useEffect(() => {
|
|
101
|
-
if (!client || !isConnected) return
|
|
102
|
-
|
|
103
|
-
const userId = client.userID
|
|
104
|
-
if (!userId) return
|
|
105
|
-
|
|
104
|
+
if (!client || !isConnected) return
|
|
105
|
+
|
|
106
|
+
const userId = client.userID
|
|
107
|
+
if (!userId) return
|
|
108
|
+
|
|
106
109
|
// Prevent repeated sync for the same user
|
|
107
|
-
if (syncedRef.current === userId) return
|
|
110
|
+
if (syncedRef.current === userId) return
|
|
108
111
|
|
|
109
|
-
syncChannels()
|
|
110
|
-
}, [client, isConnected, syncChannels])
|
|
112
|
+
syncChannels()
|
|
113
|
+
}, [client, isConnected, syncChannels])
|
|
111
114
|
|
|
112
|
-
const handleChannelSelect = useCallback(
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
115
|
+
const handleChannelSelect = useCallback(
|
|
116
|
+
(channel: Channel) => {
|
|
117
|
+
setSelectedChannel(channel)
|
|
118
|
+
onChannelSelect?.(channel)
|
|
119
|
+
},
|
|
120
|
+
[onChannelSelect]
|
|
121
|
+
)
|
|
116
122
|
|
|
117
123
|
const handleBackToChannelList = useCallback(() => {
|
|
118
|
-
setSelectedChannel(null)
|
|
119
|
-
}, [])
|
|
124
|
+
setSelectedChannel(null)
|
|
125
|
+
}, [])
|
|
120
126
|
|
|
121
127
|
const handleStartConversation = useCallback(() => {
|
|
122
128
|
if (participantSource) {
|
|
123
|
-
setPickerKey(prev => prev + 1)
|
|
124
|
-
setShowParticipantPicker(true)
|
|
125
|
-
participantPickerRef.current?.showModal()
|
|
129
|
+
setPickerKey((prev) => prev + 1) // Increment key to force remount
|
|
130
|
+
setShowParticipantPicker(true)
|
|
131
|
+
participantPickerRef.current?.showModal()
|
|
126
132
|
}
|
|
127
|
-
}, [participantSource])
|
|
133
|
+
}, [participantSource])
|
|
128
134
|
|
|
129
|
-
const handleSelectParticipant = useCallback(
|
|
130
|
-
|
|
135
|
+
const handleSelectParticipant = useCallback(
|
|
136
|
+
async (participant: Participant) => {
|
|
137
|
+
if (!service) return
|
|
131
138
|
|
|
132
|
-
try {
|
|
133
|
-
if (debug) {
|
|
134
|
-
console.log('[MessagingShell] Starting conversation with:', participant.id);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const channel = await service.startChannelWithFollower({
|
|
138
|
-
id: participant.id,
|
|
139
|
-
name: participant.name,
|
|
140
|
-
email: participant.email,
|
|
141
|
-
phone: participant.phone,
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
// Show the channel
|
|
145
139
|
try {
|
|
146
|
-
|
|
140
|
+
if (debug) {
|
|
141
|
+
console.log(
|
|
142
|
+
'[MessagingShell] Starting conversation with:',
|
|
143
|
+
participant.id
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const channel = await service.startChannelWithFollower({
|
|
148
|
+
id: participant.id,
|
|
149
|
+
name: participant.name,
|
|
150
|
+
email: participant.email,
|
|
151
|
+
phone: participant.phone,
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
// Show the channel
|
|
155
|
+
try {
|
|
156
|
+
await channel.show()
|
|
157
|
+
} catch (error) {
|
|
158
|
+
console.warn('[MessagingShell] Failed to unhide channel:', error)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
setSelectedChannel(channel)
|
|
162
|
+
setShowParticipantPicker(false)
|
|
163
|
+
participantPickerRef.current?.close()
|
|
164
|
+
|
|
165
|
+
onParticipantSelect?.(participant)
|
|
147
166
|
} catch (error) {
|
|
148
|
-
console.
|
|
167
|
+
console.error('[MessagingShell] Failed to start conversation:', error)
|
|
149
168
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
participantPickerRef.current?.close();
|
|
154
|
-
|
|
155
|
-
onParticipantSelect?.(participant);
|
|
156
|
-
} catch (error) {
|
|
157
|
-
console.error('[MessagingShell] Failed to start conversation:', error);
|
|
158
|
-
}
|
|
159
|
-
}, [service, onParticipantSelect, debug]);
|
|
169
|
+
},
|
|
170
|
+
[service, onParticipantSelect, debug]
|
|
171
|
+
)
|
|
160
172
|
|
|
161
173
|
const handleCloseParticipantPicker = useCallback(() => {
|
|
162
|
-
setShowParticipantPicker(false)
|
|
163
|
-
participantPickerRef.current?.close()
|
|
164
|
-
}, [])
|
|
174
|
+
setShowParticipantPicker(false)
|
|
175
|
+
participantPickerRef.current?.close()
|
|
176
|
+
}, [])
|
|
165
177
|
|
|
166
|
-
const handleLeaveConversation = useCallback(
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
console.log('[MessagingShell] Blocking participant:', participantId);
|
|
180
|
-
}
|
|
181
|
-
setSelectedChannel(null);
|
|
182
|
-
|
|
183
|
-
// Force re-sync to update the existing participants list
|
|
184
|
-
syncedRef.current = null;
|
|
185
|
-
await syncChannels();
|
|
186
|
-
}, [syncChannels, debug]);
|
|
178
|
+
const handleLeaveConversation = useCallback(
|
|
179
|
+
async (channel: Channel) => {
|
|
180
|
+
if (debug) {
|
|
181
|
+
console.log('[MessagingShell] Leaving conversation:', channel.id)
|
|
182
|
+
}
|
|
183
|
+
setSelectedChannel(null)
|
|
184
|
+
|
|
185
|
+
// Force re-sync to update the existing participants list
|
|
186
|
+
syncedRef.current = null
|
|
187
|
+
await syncChannels()
|
|
188
|
+
},
|
|
189
|
+
[syncChannels, debug]
|
|
190
|
+
)
|
|
187
191
|
|
|
188
|
-
const
|
|
192
|
+
const handleBlockParticipant = useCallback(
|
|
193
|
+
async (participantId?: string) => {
|
|
194
|
+
if (debug) {
|
|
195
|
+
console.log('[MessagingShell] Blocking participant:', participantId)
|
|
196
|
+
}
|
|
197
|
+
setSelectedChannel(null)
|
|
198
|
+
|
|
199
|
+
// Force re-sync to update the existing participants list
|
|
200
|
+
syncedRef.current = null
|
|
201
|
+
await syncChannels()
|
|
202
|
+
},
|
|
203
|
+
[syncChannels, debug]
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
const isChannelSelected = Boolean(selectedChannel)
|
|
189
207
|
|
|
190
208
|
// Show loading state
|
|
191
209
|
if (isLoading) {
|
|
@@ -193,7 +211,7 @@ export const MessagingShell: React.FC<MessagingShellProps> = ({
|
|
|
193
211
|
<div className={classNames('h-full', className)}>
|
|
194
212
|
<LoadingState />
|
|
195
213
|
</div>
|
|
196
|
-
)
|
|
214
|
+
)
|
|
197
215
|
}
|
|
198
216
|
|
|
199
217
|
// Show error state
|
|
@@ -202,19 +220,19 @@ export const MessagingShell: React.FC<MessagingShellProps> = ({
|
|
|
202
220
|
<div className={classNames('h-full', className)}>
|
|
203
221
|
<ErrorState error={error} onRetry={refreshConnection} />
|
|
204
222
|
</div>
|
|
205
|
-
)
|
|
223
|
+
)
|
|
206
224
|
}
|
|
207
225
|
|
|
208
226
|
// Show not connected state
|
|
209
227
|
if (!isConnected || !client) {
|
|
210
228
|
return (
|
|
211
229
|
<div className={classNames('h-full', className)}>
|
|
212
|
-
<ErrorState
|
|
213
|
-
error="Not connected to messaging service"
|
|
214
|
-
onRetry={refreshConnection}
|
|
230
|
+
<ErrorState
|
|
231
|
+
error="Not connected to messaging service"
|
|
232
|
+
onRetry={refreshConnection}
|
|
215
233
|
/>
|
|
216
234
|
</div>
|
|
217
|
-
)
|
|
235
|
+
)
|
|
218
236
|
}
|
|
219
237
|
|
|
220
238
|
return (
|
|
@@ -225,7 +243,8 @@ export const MessagingShell: React.FC<MessagingShellProps> = ({
|
|
|
225
243
|
className={classNames(
|
|
226
244
|
'min-h-0 min-w-0 bg-white lg:bg-chalk lg:flex lg:flex-col lg:border-r lg:border-sand',
|
|
227
245
|
{
|
|
228
|
-
'hidden lg:flex lg:w-80 lg:min-w-[280px] lg:max-w-[360px]':
|
|
246
|
+
'hidden lg:flex lg:w-80 lg:min-w-[280px] lg:max-w-[360px]':
|
|
247
|
+
isChannelSelected,
|
|
229
248
|
'flex flex-col w-full lg:flex-1 lg:max-w-2xl': !isChannelSelected,
|
|
230
249
|
}
|
|
231
250
|
)}
|
|
@@ -233,7 +252,9 @@ export const MessagingShell: React.FC<MessagingShellProps> = ({
|
|
|
233
252
|
<ChannelList
|
|
234
253
|
onChannelSelect={handleChannelSelect}
|
|
235
254
|
selectedChannel={selectedChannel || undefined}
|
|
236
|
-
showStartConversation={
|
|
255
|
+
showStartConversation={
|
|
256
|
+
showStartConversation && Boolean(participantSource)
|
|
257
|
+
}
|
|
237
258
|
onStartConversation={handleStartConversation}
|
|
238
259
|
participantLabel={participantLabel}
|
|
239
260
|
/>
|
|
@@ -243,7 +264,7 @@ export const MessagingShell: React.FC<MessagingShellProps> = ({
|
|
|
243
264
|
<div
|
|
244
265
|
className={classNames('flex-1 flex-col min-w-0 min-h-0', {
|
|
245
266
|
'hidden lg:flex': !isChannelSelected,
|
|
246
|
-
|
|
267
|
+
flex: isChannelSelected,
|
|
247
268
|
})}
|
|
248
269
|
>
|
|
249
270
|
{selectedChannel ? (
|
|
@@ -261,7 +282,9 @@ export const MessagingShell: React.FC<MessagingShellProps> = ({
|
|
|
261
282
|
) : (
|
|
262
283
|
<EmptyState
|
|
263
284
|
hasChannels={hasChannels}
|
|
264
|
-
onStartConversation={
|
|
285
|
+
onStartConversation={
|
|
286
|
+
showStartConversation ? handleStartConversation : undefined
|
|
287
|
+
}
|
|
265
288
|
participantLabel={participantLabel}
|
|
266
289
|
/>
|
|
267
290
|
)}
|
|
@@ -270,12 +293,13 @@ export const MessagingShell: React.FC<MessagingShellProps> = ({
|
|
|
270
293
|
|
|
271
294
|
{/* Participant Picker Dialog */}
|
|
272
295
|
{participantSource && (
|
|
296
|
+
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions
|
|
273
297
|
<dialog
|
|
274
298
|
ref={participantPickerRef}
|
|
275
299
|
className="mes-dialog"
|
|
276
300
|
onClick={(e) => {
|
|
277
301
|
if (e.target === participantPickerRef.current) {
|
|
278
|
-
handleCloseParticipantPicker()
|
|
302
|
+
handleCloseParticipantPicker()
|
|
279
303
|
}
|
|
280
304
|
}}
|
|
281
305
|
onClose={handleCloseParticipantPicker}
|
|
@@ -294,5 +318,5 @@ export const MessagingShell: React.FC<MessagingShellProps> = ({
|
|
|
294
318
|
</dialog>
|
|
295
319
|
)}
|
|
296
320
|
</div>
|
|
297
|
-
)
|
|
298
|
-
}
|
|
321
|
+
)
|
|
322
|
+
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
-
import { ParticipantItem } from './ParticipantItem';
|
|
3
|
-
import type { Participant } from '../../types';
|
|
4
2
|
import React from 'react';
|
|
5
3
|
|
|
4
|
+
import type { Participant } from '../../types';
|
|
5
|
+
|
|
6
|
+
import { ParticipantItem } from './ParticipantItem';
|
|
7
|
+
|
|
6
8
|
const meta: Meta<typeof ParticipantItem> = {
|
|
7
9
|
title: 'ParticipantItem',
|
|
8
10
|
component: ParticipantItem,
|
|
@@ -1,21 +1,27 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import { SpinnerGapIcon } from '@phosphor-icons/react/dist/csr/SpinnerGap';
|
|
4
|
-
import { ChatCircleDotsIcon } from '@phosphor-icons/react/dist/csr/ChatCircleDots';
|
|
5
|
-
import { Avatar } from '../Avatar';
|
|
1
|
+
import { ChatCircleDotsIcon, SpinnerGapIcon } from '@phosphor-icons/react'
|
|
2
|
+
import React from 'react'
|
|
6
3
|
|
|
4
|
+
import type { Participant } from '../../types'
|
|
5
|
+
import { Avatar } from '../Avatar'
|
|
7
6
|
|
|
8
7
|
type ParticipantItemProps = {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
8
|
+
participant: Participant
|
|
9
|
+
handleSelectParticipant: (participant: Participant) => void
|
|
10
|
+
handleKeyDown: (event: React.KeyboardEvent, participant: Participant) => void
|
|
11
|
+
displayName: string
|
|
12
|
+
displaySecondary?: string
|
|
13
|
+
startingChatWithId?: string | null
|
|
14
|
+
}
|
|
16
15
|
|
|
17
|
-
export const ParticipantItem: React.FC<ParticipantItemProps> = ({
|
|
18
|
-
|
|
16
|
+
export const ParticipantItem: React.FC<ParticipantItemProps> = ({
|
|
17
|
+
participant,
|
|
18
|
+
handleSelectParticipant,
|
|
19
|
+
handleKeyDown,
|
|
20
|
+
displayName,
|
|
21
|
+
displaySecondary,
|
|
22
|
+
startingChatWithId,
|
|
23
|
+
}) => (
|
|
24
|
+
<li key={participant.id}>
|
|
19
25
|
<button
|
|
20
26
|
type="button"
|
|
21
27
|
onClick={() => handleSelectParticipant(participant)}
|
|
@@ -31,20 +37,18 @@ export const ParticipantItem: React.FC<ParticipantItemProps> = ({ participant, h
|
|
|
31
37
|
image={participant.image}
|
|
32
38
|
size={40}
|
|
33
39
|
/>
|
|
34
|
-
|
|
40
|
+
|
|
35
41
|
{/* Info */}
|
|
36
42
|
<div className="flex-1 min-w-0">
|
|
37
43
|
<h4 className="text-sm font-medium text-charcoal truncate">
|
|
38
44
|
{displayName}
|
|
39
45
|
</h4>
|
|
40
46
|
{displaySecondary && (
|
|
41
|
-
<p className="text-xs text-stone truncate">
|
|
42
|
-
{displaySecondary}
|
|
43
|
-
</p>
|
|
47
|
+
<p className="text-xs text-stone truncate">{displaySecondary}</p>
|
|
44
48
|
)}
|
|
45
49
|
</div>
|
|
46
50
|
</div>
|
|
47
|
-
|
|
51
|
+
|
|
48
52
|
{/* Icon */}
|
|
49
53
|
<div className="flex-shrink-0">
|
|
50
54
|
{startingChatWithId === participant.id ? (
|
|
@@ -52,8 +56,8 @@ export const ParticipantItem: React.FC<ParticipantItemProps> = ({ participant, h
|
|
|
52
56
|
) : (
|
|
53
57
|
<ChatCircleDotsIcon className="h-5 w-5 text-stone" />
|
|
54
58
|
)}
|
|
55
|
-
</div>
|
|
59
|
+
</div>
|
|
56
60
|
</div>
|
|
57
61
|
</button>
|
|
58
62
|
</li>
|
|
59
|
-
|
|
63
|
+
)
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { Meta, StoryFn } from '@storybook/react'
|
|
2
|
-
import { ParticipantPicker } from './index'
|
|
3
|
-
import { mockParticipantSource } from '../../stories/mocks'
|
|
4
2
|
import React from 'react'
|
|
5
3
|
|
|
4
|
+
import { mockParticipantSource } from '../../stories/mocks'
|
|
5
|
+
|
|
6
|
+
import { ParticipantPicker } from './index'
|
|
7
|
+
|
|
6
8
|
type ComponentProps = React.ComponentProps<typeof ParticipantPicker>
|
|
7
9
|
|
|
8
10
|
const meta: Meta<ComponentProps> = {
|