@linktr.ee/messaging-react 1.38.0-rc-1777583423 → 1.39.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/{Card-DoNJA-jg.js → Card-BjTfMGsC.js} +2 -2
- package/dist/{Card-DoNJA-jg.js.map → Card-BjTfMGsC.js.map} +1 -1
- package/dist/{Card-BlXnKGaR.js → Card-D3E-WTRO.js} +2 -2
- package/dist/{Card-BlXnKGaR.js.map → Card-D3E-WTRO.js.map} +1 -1
- package/dist/{index-jnKl3mQ0.js → index-j4j9UfK1.js} +974 -1262
- package/dist/index-j4j9UfK1.js.map +1 -0
- package/dist/index.d.ts +0 -60
- package/dist/index.js +9 -11
- package/package.json +1 -1
- package/src/components/AttachmentCard/index.tsx +11 -9
- package/src/components/ChannelList/ChannelList.stories.tsx +2 -7
- package/src/components/MediaMessage/MediaMessage.test.tsx +34 -1
- package/src/components/MediaMessage/index.tsx +18 -5
- package/src/components/MessagingShell/index.tsx +2 -110
- package/src/index.ts +0 -4
- package/src/types.ts +0 -37
- package/dist/index-jnKl3mQ0.js.map +0 -1
- package/src/components/ParticipantPicker/ParticipantItem.stories.tsx +0 -190
- package/src/components/ParticipantPicker/ParticipantItem.tsx +0 -63
- package/src/components/ParticipantPicker/ParticipantPicker.stories.tsx +0 -56
- package/src/components/ParticipantPicker/ParticipantPicker.tsx +0 -229
- package/src/components/ParticipantPicker/index.tsx +0 -262
- package/src/hooks/useParticipants.ts +0 -94
|
@@ -1,262 +0,0 @@
|
|
|
1
|
-
import { ChatCircleDotsIcon, SpinnerGapIcon } from '@phosphor-icons/react'
|
|
2
|
-
import classNames from 'classnames'
|
|
3
|
-
import React, { useCallback, useEffect, useState, useRef } from 'react'
|
|
4
|
-
|
|
5
|
-
import { useMessagingContext } from '../../providers/MessagingProvider'
|
|
6
|
-
import type { ParticipantPickerProps, Participant } from '../../types'
|
|
7
|
-
import { Avatar } from '../Avatar'
|
|
8
|
-
import { CloseButton } from '../CloseButton'
|
|
9
|
-
import { SearchInput } from '../SearchInput'
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Generic participant picker component for starting conversations
|
|
13
|
-
*/
|
|
14
|
-
export const ParticipantPicker: React.FC<ParticipantPickerProps> = ({
|
|
15
|
-
participantSource,
|
|
16
|
-
onSelectParticipant,
|
|
17
|
-
onClose,
|
|
18
|
-
existingParticipantIds = new Set(),
|
|
19
|
-
participantLabel = 'participants',
|
|
20
|
-
searchPlaceholder = 'Search participants...',
|
|
21
|
-
className,
|
|
22
|
-
}) => {
|
|
23
|
-
const { debug } = useMessagingContext()
|
|
24
|
-
const [searchQuery, setSearchQuery] = useState('')
|
|
25
|
-
const [participants, setParticipants] = useState<Participant[]>([])
|
|
26
|
-
const [loading, setLoading] = useState(false)
|
|
27
|
-
const [error, setError] = useState<string | null>(null)
|
|
28
|
-
const [startingChatWithId, setStartingChatWithId] = useState<string | null>(
|
|
29
|
-
null
|
|
30
|
-
)
|
|
31
|
-
|
|
32
|
-
// Track if we've already loaded participants to prevent repeated loading
|
|
33
|
-
const loadedRef = useRef(false)
|
|
34
|
-
|
|
35
|
-
// New source instance should be allowed to load again
|
|
36
|
-
useEffect(() => {
|
|
37
|
-
loadedRef.current = false
|
|
38
|
-
}, [participantSource])
|
|
39
|
-
|
|
40
|
-
/* eslint-disable react-hooks/exhaustive-deps -- syncs with participantSource + debug; inner async uses participantSource.loadParticipants */
|
|
41
|
-
// Load participants initially - wait for participantSource to finish loading first
|
|
42
|
-
useEffect(() => {
|
|
43
|
-
// Wait for the participantSource to finish loading before we try to load participants
|
|
44
|
-
if (participantSource.loading) {
|
|
45
|
-
if (debug) {
|
|
46
|
-
console.log(
|
|
47
|
-
'[ParticipantPicker] Waiting for participant source to finish loading...'
|
|
48
|
-
)
|
|
49
|
-
}
|
|
50
|
-
return
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (loadedRef.current) return // Prevent multiple loads
|
|
54
|
-
|
|
55
|
-
const loadInitialParticipants = async () => {
|
|
56
|
-
if (debug) {
|
|
57
|
-
console.log('[ParticipantPicker] Loading initial participants...')
|
|
58
|
-
}
|
|
59
|
-
setLoading(true)
|
|
60
|
-
setError(null)
|
|
61
|
-
|
|
62
|
-
try {
|
|
63
|
-
const result = await participantSource.loadParticipants({
|
|
64
|
-
search: '', // Load all participants initially
|
|
65
|
-
limit: 100,
|
|
66
|
-
})
|
|
67
|
-
setParticipants(result.participants)
|
|
68
|
-
loadedRef.current = true // Mark as loaded
|
|
69
|
-
if (debug) {
|
|
70
|
-
console.log(
|
|
71
|
-
'[ParticipantPicker] Participants loaded successfully:',
|
|
72
|
-
result.participants.length
|
|
73
|
-
)
|
|
74
|
-
}
|
|
75
|
-
} catch (err) {
|
|
76
|
-
const errorMessage =
|
|
77
|
-
err instanceof Error ? err.message : 'Failed to load participants'
|
|
78
|
-
setError(errorMessage)
|
|
79
|
-
console.error('[ParticipantPicker] Failed to load participants:', err)
|
|
80
|
-
// Don't mark as loaded on error, allow retry
|
|
81
|
-
} finally {
|
|
82
|
-
setLoading(false)
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
loadInitialParticipants()
|
|
87
|
-
}, [participantSource, debug])
|
|
88
|
-
/* eslint-enable react-hooks/exhaustive-deps */
|
|
89
|
-
|
|
90
|
-
// Filter participants by search query and existing participants
|
|
91
|
-
const availableParticipants = participants
|
|
92
|
-
.filter((participant) => !existingParticipantIds.has(participant.id))
|
|
93
|
-
.filter((participant) => {
|
|
94
|
-
if (!searchQuery) return true
|
|
95
|
-
const searchLower = searchQuery.toLowerCase()
|
|
96
|
-
return (
|
|
97
|
-
participant.name.toLowerCase().includes(searchLower) ||
|
|
98
|
-
participant.email?.toLowerCase().includes(searchLower) ||
|
|
99
|
-
false
|
|
100
|
-
)
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
const handleSelectParticipant = useCallback(
|
|
104
|
-
async (participant: Participant) => {
|
|
105
|
-
if (startingChatWithId) return // Prevent multiple clicks
|
|
106
|
-
|
|
107
|
-
setStartingChatWithId(participant.id)
|
|
108
|
-
try {
|
|
109
|
-
await onSelectParticipant(participant)
|
|
110
|
-
} catch (error) {
|
|
111
|
-
console.error('[ParticipantPicker] Failed to start chat:', error)
|
|
112
|
-
// Reset the loading state on error
|
|
113
|
-
setStartingChatWithId(null)
|
|
114
|
-
}
|
|
115
|
-
// Note: Don't reset startingChatWithId on success because the dialog will close
|
|
116
|
-
},
|
|
117
|
-
[onSelectParticipant, startingChatWithId]
|
|
118
|
-
)
|
|
119
|
-
|
|
120
|
-
const handleKeyDown = (
|
|
121
|
-
event: React.KeyboardEvent,
|
|
122
|
-
participant: Participant
|
|
123
|
-
) => {
|
|
124
|
-
if (event.key === 'Enter' || event.key === ' ') {
|
|
125
|
-
event.preventDefault()
|
|
126
|
-
handleSelectParticipant(participant)
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
return (
|
|
131
|
-
<div className={classNames('flex flex-col h-full', className)}>
|
|
132
|
-
{/* Header */}
|
|
133
|
-
<div className="px-4 py-4 border-b border-sand bg-chalk">
|
|
134
|
-
<div className="flex items-center justify-between mb-3">
|
|
135
|
-
<h2 className="text-lg font-semibold text-charcoal">
|
|
136
|
-
Start a new Conversation
|
|
137
|
-
</h2>
|
|
138
|
-
<CloseButton onClick={onClose} />
|
|
139
|
-
</div>
|
|
140
|
-
|
|
141
|
-
<p className="text-xs text-stone mb-3">
|
|
142
|
-
Select a {participantLabel.slice(0, -1)} to start messaging (
|
|
143
|
-
{availableParticipants.length} available)
|
|
144
|
-
{participantSource.totalCount !== undefined &&
|
|
145
|
-
` • ${participantSource.totalCount} ${participantLabel} total`}
|
|
146
|
-
</p>
|
|
147
|
-
|
|
148
|
-
<SearchInput
|
|
149
|
-
searchQuery={searchQuery}
|
|
150
|
-
setSearchQuery={setSearchQuery}
|
|
151
|
-
placeholder={searchPlaceholder}
|
|
152
|
-
/>
|
|
153
|
-
</div>
|
|
154
|
-
|
|
155
|
-
{/* Error State */}
|
|
156
|
-
{error && (
|
|
157
|
-
<div className="p-4 text-sm text-danger bg-danger-alt">
|
|
158
|
-
Error loading {participantLabel}: {error}
|
|
159
|
-
</div>
|
|
160
|
-
)}
|
|
161
|
-
|
|
162
|
-
{/* Participants List */}
|
|
163
|
-
<div className="flex-1 overflow-auto">
|
|
164
|
-
{loading && availableParticipants.length === 0 ? (
|
|
165
|
-
<div className="h-32 flex items-center justify-center">
|
|
166
|
-
<div className="flex items-center space-x-2">
|
|
167
|
-
<div className="w-4 h-4 animate-spin rounded-full border-2 border-primary border-t-transparent"></div>
|
|
168
|
-
<span className="text-sm text-stone">
|
|
169
|
-
Loading {participantLabel}...
|
|
170
|
-
</span>
|
|
171
|
-
</div>
|
|
172
|
-
</div>
|
|
173
|
-
) : availableParticipants.length === 0 ? (
|
|
174
|
-
<div className="p-6 text-center">
|
|
175
|
-
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-sand">
|
|
176
|
-
<ChatCircleDotsIcon className="h-8 w-8 text-charcoal" />
|
|
177
|
-
</div>
|
|
178
|
-
<h3 className="text-sm font-semibold text-charcoal mb-2">
|
|
179
|
-
{searchQuery
|
|
180
|
-
? `No ${participantLabel} found`
|
|
181
|
-
: participants.length > 0
|
|
182
|
-
? `Already chatting with all ${participantLabel}`
|
|
183
|
-
: `No ${participantLabel} yet`}
|
|
184
|
-
</h3>
|
|
185
|
-
<p className="text-xs text-stone">
|
|
186
|
-
{searchQuery
|
|
187
|
-
? 'Try a different search term'
|
|
188
|
-
: participants.length > 0
|
|
189
|
-
? `You have existing conversations with all your ${participantLabel}`
|
|
190
|
-
: `${participantLabel.charAt(0).toUpperCase() + participantLabel.slice(1)} will appear here`}
|
|
191
|
-
</p>
|
|
192
|
-
</div>
|
|
193
|
-
) : (
|
|
194
|
-
<ul className="space-y-0">
|
|
195
|
-
{availableParticipants.map((participant) => {
|
|
196
|
-
const displayName =
|
|
197
|
-
participant.name || participant.email || participant.id
|
|
198
|
-
const displaySecondary =
|
|
199
|
-
participant.email && participant.name
|
|
200
|
-
? participant.email
|
|
201
|
-
: participant.phone
|
|
202
|
-
|
|
203
|
-
return (
|
|
204
|
-
<li key={participant.id}>
|
|
205
|
-
<button
|
|
206
|
-
type="button"
|
|
207
|
-
onClick={() => handleSelectParticipant(participant)}
|
|
208
|
-
onKeyDown={(e) => handleKeyDown(e, participant)}
|
|
209
|
-
className="w-full px-4 py-3 hover:bg-sand transition-colors border-b border-sand text-left focus-ring"
|
|
210
|
-
>
|
|
211
|
-
<div className="flex items-center justify-between">
|
|
212
|
-
<div className="flex items-center space-x-3 flex-1 min-w-0">
|
|
213
|
-
{/* Avatar */}
|
|
214
|
-
<Avatar
|
|
215
|
-
id={participant.id}
|
|
216
|
-
name={displayName}
|
|
217
|
-
image={participant.image}
|
|
218
|
-
size={40}
|
|
219
|
-
/>
|
|
220
|
-
|
|
221
|
-
{/* Info */}
|
|
222
|
-
<div className="flex-1 min-w-0">
|
|
223
|
-
<h4 className="text-sm font-medium text-charcoal truncate">
|
|
224
|
-
{displayName}
|
|
225
|
-
</h4>
|
|
226
|
-
{displaySecondary && (
|
|
227
|
-
<p className="text-xs text-stone truncate">
|
|
228
|
-
{displaySecondary}
|
|
229
|
-
</p>
|
|
230
|
-
)}
|
|
231
|
-
</div>
|
|
232
|
-
</div>
|
|
233
|
-
|
|
234
|
-
{/* Icon */}
|
|
235
|
-
<div className="flex-shrink-0">
|
|
236
|
-
{startingChatWithId === participant.id ? (
|
|
237
|
-
<SpinnerGapIcon className="h-5 w-5 text-primary animate-spin" />
|
|
238
|
-
) : (
|
|
239
|
-
<ChatCircleDotsIcon className="h-5 w-5 text-stone" />
|
|
240
|
-
)}
|
|
241
|
-
</div>
|
|
242
|
-
</div>
|
|
243
|
-
</button>
|
|
244
|
-
</li>
|
|
245
|
-
)
|
|
246
|
-
})}
|
|
247
|
-
|
|
248
|
-
{/* Loading indicator */}
|
|
249
|
-
{loading && (
|
|
250
|
-
<li className="p-4 flex justify-center">
|
|
251
|
-
<div className="flex items-center space-x-2">
|
|
252
|
-
<div className="w-4 h-4 animate-spin rounded-full border-2 border-primary border-t-transparent"></div>
|
|
253
|
-
<span className="text-sm text-stone">Loading more...</span>
|
|
254
|
-
</div>
|
|
255
|
-
</li>
|
|
256
|
-
)}
|
|
257
|
-
</ul>
|
|
258
|
-
)}
|
|
259
|
-
</div>
|
|
260
|
-
</div>
|
|
261
|
-
)
|
|
262
|
-
}
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect, useCallback } from 'react';
|
|
2
|
-
|
|
3
|
-
import type { ParticipantSource, Participant } from '../types';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Hook for managing participant loading with search and pagination
|
|
7
|
-
*/
|
|
8
|
-
export const useParticipants = (
|
|
9
|
-
participantSource: ParticipantSource,
|
|
10
|
-
options: {
|
|
11
|
-
initialSearch?: string;
|
|
12
|
-
pageSize?: number;
|
|
13
|
-
} = {}
|
|
14
|
-
) => {
|
|
15
|
-
const { initialSearch = '', pageSize = 20 } = options;
|
|
16
|
-
|
|
17
|
-
const [participants, setParticipants] = useState<Participant[]>([]);
|
|
18
|
-
const [loading, setLoading] = useState(false);
|
|
19
|
-
const [error, setError] = useState<string | null>(null);
|
|
20
|
-
const [searchQuery, setSearchQuery] = useState(initialSearch);
|
|
21
|
-
const [hasMore, setHasMore] = useState(true);
|
|
22
|
-
const [cursor, setCursor] = useState<string | undefined>();
|
|
23
|
-
|
|
24
|
-
// Load participants with current search query
|
|
25
|
-
const loadParticipants = useCallback(async (
|
|
26
|
-
reset = false,
|
|
27
|
-
customSearch?: string
|
|
28
|
-
) => {
|
|
29
|
-
if (loading) return;
|
|
30
|
-
|
|
31
|
-
const search = customSearch !== undefined ? customSearch : searchQuery;
|
|
32
|
-
|
|
33
|
-
setLoading(true);
|
|
34
|
-
setError(null);
|
|
35
|
-
|
|
36
|
-
try {
|
|
37
|
-
const result = await participantSource.loadParticipants({
|
|
38
|
-
search: search || undefined,
|
|
39
|
-
limit: pageSize,
|
|
40
|
-
cursor: reset ? undefined : cursor,
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
setParticipants(prev =>
|
|
44
|
-
reset ? result.participants : [...prev, ...result.participants]
|
|
45
|
-
);
|
|
46
|
-
setHasMore(result.hasMore);
|
|
47
|
-
setCursor(result.nextCursor);
|
|
48
|
-
} catch (err) {
|
|
49
|
-
const errorMessage = err instanceof Error ? err.message : 'Failed to load participants';
|
|
50
|
-
setError(errorMessage);
|
|
51
|
-
console.error('[useParticipants] Load error:', err);
|
|
52
|
-
} finally {
|
|
53
|
-
setLoading(false);
|
|
54
|
-
}
|
|
55
|
-
}, [participantSource, searchQuery, cursor, pageSize, loading]);
|
|
56
|
-
|
|
57
|
-
// Load more participants (pagination)
|
|
58
|
-
const loadMore = useCallback(() => {
|
|
59
|
-
if (hasMore && !loading) {
|
|
60
|
-
loadParticipants(false);
|
|
61
|
-
}
|
|
62
|
-
}, [hasMore, loading, loadParticipants]);
|
|
63
|
-
|
|
64
|
-
// Search participants
|
|
65
|
-
const search = useCallback((query: string) => {
|
|
66
|
-
setSearchQuery(query);
|
|
67
|
-
setCursor(undefined);
|
|
68
|
-
loadParticipants(true, query);
|
|
69
|
-
}, [loadParticipants]);
|
|
70
|
-
|
|
71
|
-
// Refresh participants
|
|
72
|
-
const refresh = useCallback(() => {
|
|
73
|
-
setCursor(undefined);
|
|
74
|
-
loadParticipants(true);
|
|
75
|
-
}, [loadParticipants]);
|
|
76
|
-
|
|
77
|
-
/* eslint-disable react-hooks/exhaustive-deps -- initial load only; `loadParticipants` changes whenever `loading` flips */
|
|
78
|
-
useEffect(() => {
|
|
79
|
-
loadParticipants(true);
|
|
80
|
-
}, [participantSource.loadParticipants]);
|
|
81
|
-
/* eslint-enable react-hooks/exhaustive-deps */
|
|
82
|
-
|
|
83
|
-
return {
|
|
84
|
-
participants,
|
|
85
|
-
loading,
|
|
86
|
-
error,
|
|
87
|
-
searchQuery,
|
|
88
|
-
hasMore,
|
|
89
|
-
totalCount: participantSource.totalCount,
|
|
90
|
-
loadMore,
|
|
91
|
-
search,
|
|
92
|
-
refresh,
|
|
93
|
-
};
|
|
94
|
-
};
|