@linktr.ee/messaging-react 1.38.0 → 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-RgHsp9x1.js → Card-BjTfMGsC.js} +2 -2
- package/dist/{Card-RgHsp9x1.js.map → Card-BjTfMGsC.js.map} +1 -1
- package/dist/{Card-DwgUtqsA.js → Card-D3E-WTRO.js} +2 -2
- package/dist/{Card-DwgUtqsA.js.map → Card-D3E-WTRO.js.map} +1 -1
- package/dist/{index-B_4pciGp.js → index-j4j9UfK1.js} +971 -1260
- 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/ChannelList/ChannelList.stories.tsx +2 -7
- package/src/components/MessagingShell/index.tsx +2 -110
- package/src/index.ts +0 -4
- package/src/types.ts +0 -37
- package/dist/index-B_4pciGp.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,190 +0,0 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
-
import React from 'react';
|
|
3
|
-
|
|
4
|
-
import type { Participant } from '../../types';
|
|
5
|
-
|
|
6
|
-
import { ParticipantItem } from './ParticipantItem';
|
|
7
|
-
|
|
8
|
-
const meta: Meta<typeof ParticipantItem> = {
|
|
9
|
-
title: 'ParticipantItem',
|
|
10
|
-
component: ParticipantItem,
|
|
11
|
-
parameters: {
|
|
12
|
-
// layout: 'centered',
|
|
13
|
-
},
|
|
14
|
-
tags: ['autodocs'],
|
|
15
|
-
decorators: [
|
|
16
|
-
(Story) => (
|
|
17
|
-
<ul className="w-96 border border-sand rounded-lg overflow-hidden">
|
|
18
|
-
<Story />
|
|
19
|
-
</ul>
|
|
20
|
-
),
|
|
21
|
-
],
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
export default meta;
|
|
25
|
-
type Story = StoryObj<typeof ParticipantItem>;
|
|
26
|
-
|
|
27
|
-
const mockParticipant: Participant = {
|
|
28
|
-
id: 'user-123',
|
|
29
|
-
name: 'Sarah Johnson',
|
|
30
|
-
email: 'sarah.johnson@example.com',
|
|
31
|
-
image: 'https://picsum.photos/id/237/200/200',
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
const mockHandleSelect = (participant: Participant) => {
|
|
35
|
-
console.log('Selected participant:', participant);
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
const mockHandleKeyDown = (event: React.KeyboardEvent, participant: Participant) => {
|
|
39
|
-
console.log('Key pressed:', event.key, 'on participant:', participant);
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
export const Default: Story = {
|
|
43
|
-
args: {
|
|
44
|
-
participant: mockParticipant,
|
|
45
|
-
handleSelectParticipant: mockHandleSelect,
|
|
46
|
-
handleKeyDown: mockHandleKeyDown,
|
|
47
|
-
displayName: 'Sarah Johnson',
|
|
48
|
-
displaySecondary: 'sarah.johnson@example.com',
|
|
49
|
-
},
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
export const WithPhone: Story = {
|
|
53
|
-
args: {
|
|
54
|
-
participant: {
|
|
55
|
-
...mockParticipant,
|
|
56
|
-
phone: '+1 (555) 123-4567',
|
|
57
|
-
},
|
|
58
|
-
handleSelectParticipant: mockHandleSelect,
|
|
59
|
-
handleKeyDown: mockHandleKeyDown,
|
|
60
|
-
displayName: 'Sarah Johnson',
|
|
61
|
-
displaySecondary: '+1 (555) 123-4567',
|
|
62
|
-
},
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
export const NoSecondaryInfo: Story = {
|
|
66
|
-
args: {
|
|
67
|
-
participant: {
|
|
68
|
-
id: 'user-456',
|
|
69
|
-
name: 'John Doe',
|
|
70
|
-
},
|
|
71
|
-
handleSelectParticipant: mockHandleSelect,
|
|
72
|
-
handleKeyDown: mockHandleKeyDown,
|
|
73
|
-
displayName: 'John Doe',
|
|
74
|
-
},
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
export const Loading: Story = {
|
|
78
|
-
args: {
|
|
79
|
-
participant: mockParticipant,
|
|
80
|
-
handleSelectParticipant: mockHandleSelect,
|
|
81
|
-
handleKeyDown: mockHandleKeyDown,
|
|
82
|
-
displayName: 'Sarah Johnson',
|
|
83
|
-
displaySecondary: 'sarah.johnson@example.com',
|
|
84
|
-
startingChatWithId: 'user-123',
|
|
85
|
-
},
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
export const LongName: Story = {
|
|
89
|
-
args: {
|
|
90
|
-
participant: {
|
|
91
|
-
id: 'user-789',
|
|
92
|
-
name: 'Alexander Christopher Wellington-Montgomery III',
|
|
93
|
-
email: 'alexander.christopher.wellington@example.com',
|
|
94
|
-
},
|
|
95
|
-
handleSelectParticipant: mockHandleSelect,
|
|
96
|
-
handleKeyDown: mockHandleKeyDown,
|
|
97
|
-
displayName: 'Alexander Christopher Wellington-Montgomery III',
|
|
98
|
-
displaySecondary: 'alexander.christopher.wellington@example.com',
|
|
99
|
-
},
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
export const DifferentColors: Story = {
|
|
103
|
-
render: () => (
|
|
104
|
-
<div className="space-y-0 w-96">
|
|
105
|
-
<ParticipantItem
|
|
106
|
-
participant={{ id: '1', name: 'Alice Anderson' }}
|
|
107
|
-
handleSelectParticipant={mockHandleSelect}
|
|
108
|
-
handleKeyDown={mockHandleKeyDown}
|
|
109
|
-
displayName="Alice Anderson"
|
|
110
|
-
displaySecondary="alice@example.com"
|
|
111
|
-
/>
|
|
112
|
-
<ParticipantItem
|
|
113
|
-
participant={{ id: '2', name: 'Bob Brown' }}
|
|
114
|
-
handleSelectParticipant={mockHandleSelect}
|
|
115
|
-
handleKeyDown={mockHandleKeyDown}
|
|
116
|
-
displayName="Bob Brown"
|
|
117
|
-
displaySecondary="bob@example.com"
|
|
118
|
-
/>
|
|
119
|
-
<ParticipantItem
|
|
120
|
-
participant={{ id: '3', name: 'Charlie Chen' }}
|
|
121
|
-
handleSelectParticipant={mockHandleSelect}
|
|
122
|
-
handleKeyDown={mockHandleKeyDown}
|
|
123
|
-
displayName="Charlie Chen"
|
|
124
|
-
displaySecondary="charlie@example.com"
|
|
125
|
-
/>
|
|
126
|
-
<ParticipantItem
|
|
127
|
-
participant={{ id: '4', name: 'Diana Davis' }}
|
|
128
|
-
handleSelectParticipant={mockHandleSelect}
|
|
129
|
-
handleKeyDown={mockHandleKeyDown}
|
|
130
|
-
displayName="Diana Davis"
|
|
131
|
-
displaySecondary="diana@example.com"
|
|
132
|
-
/>
|
|
133
|
-
</div>
|
|
134
|
-
),
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
export const MixedAvatars: Story = {
|
|
138
|
-
render: () => (
|
|
139
|
-
<div className="space-y-0 w-96">
|
|
140
|
-
<ParticipantItem
|
|
141
|
-
participant={{
|
|
142
|
-
id: '1',
|
|
143
|
-
name: 'Emma Wilson',
|
|
144
|
-
image: 'https://picsum.photos/id/64/200/200'
|
|
145
|
-
}}
|
|
146
|
-
handleSelectParticipant={mockHandleSelect}
|
|
147
|
-
handleKeyDown={mockHandleKeyDown}
|
|
148
|
-
displayName="Emma Wilson"
|
|
149
|
-
displaySecondary="emma@example.com"
|
|
150
|
-
/>
|
|
151
|
-
<ParticipantItem
|
|
152
|
-
participant={{ id: '2', name: 'Frank Miller' }}
|
|
153
|
-
handleSelectParticipant={mockHandleSelect}
|
|
154
|
-
handleKeyDown={mockHandleKeyDown}
|
|
155
|
-
displayName="Frank Miller"
|
|
156
|
-
displaySecondary="frank@example.com"
|
|
157
|
-
/>
|
|
158
|
-
<ParticipantItem
|
|
159
|
-
participant={{
|
|
160
|
-
id: '3',
|
|
161
|
-
name: 'Grace Lee',
|
|
162
|
-
image: 'https://picsum.photos/id/91/200/200'
|
|
163
|
-
}}
|
|
164
|
-
handleSelectParticipant={mockHandleSelect}
|
|
165
|
-
handleKeyDown={mockHandleKeyDown}
|
|
166
|
-
displayName="Grace Lee"
|
|
167
|
-
displaySecondary="grace@example.com"
|
|
168
|
-
/>
|
|
169
|
-
<ParticipantItem
|
|
170
|
-
participant={{ id: '4', name: 'Henry Taylor' }}
|
|
171
|
-
handleSelectParticipant={mockHandleSelect}
|
|
172
|
-
handleKeyDown={mockHandleKeyDown}
|
|
173
|
-
displayName="Henry Taylor"
|
|
174
|
-
displaySecondary="henry@example.com"
|
|
175
|
-
/>
|
|
176
|
-
<ParticipantItem
|
|
177
|
-
participant={{
|
|
178
|
-
id: '5',
|
|
179
|
-
name: 'Iris Chen',
|
|
180
|
-
image: 'https://picsum.photos/id/177/200/200'
|
|
181
|
-
}}
|
|
182
|
-
handleSelectParticipant={mockHandleSelect}
|
|
183
|
-
handleKeyDown={mockHandleKeyDown}
|
|
184
|
-
displayName="Iris Chen"
|
|
185
|
-
displaySecondary="iris@example.com"
|
|
186
|
-
/>
|
|
187
|
-
</div>
|
|
188
|
-
),
|
|
189
|
-
};
|
|
190
|
-
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import { ChatCircleDotsIcon, SpinnerGapIcon } from '@phosphor-icons/react'
|
|
2
|
-
import React from 'react'
|
|
3
|
-
|
|
4
|
-
import type { Participant } from '../../types'
|
|
5
|
-
import { Avatar } from '../Avatar'
|
|
6
|
-
|
|
7
|
-
type ParticipantItemProps = {
|
|
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
|
-
}
|
|
15
|
-
|
|
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}>
|
|
25
|
-
<button
|
|
26
|
-
type="button"
|
|
27
|
-
onClick={() => handleSelectParticipant(participant)}
|
|
28
|
-
onKeyDown={(e) => handleKeyDown(e, participant)}
|
|
29
|
-
className="w-full px-4 py-3 hover:bg-sand transition-colors border-b border-sand text-left focus-ring"
|
|
30
|
-
>
|
|
31
|
-
<div className="flex items-center justify-between">
|
|
32
|
-
<div className="flex items-center space-x-3 flex-1 min-w-0">
|
|
33
|
-
{/* Avatar */}
|
|
34
|
-
<Avatar
|
|
35
|
-
id={participant.id}
|
|
36
|
-
name={displayName}
|
|
37
|
-
image={participant.image}
|
|
38
|
-
size={40}
|
|
39
|
-
/>
|
|
40
|
-
|
|
41
|
-
{/* Info */}
|
|
42
|
-
<div className="flex-1 min-w-0">
|
|
43
|
-
<h4 className="text-sm font-medium text-charcoal truncate">
|
|
44
|
-
{displayName}
|
|
45
|
-
</h4>
|
|
46
|
-
{displaySecondary && (
|
|
47
|
-
<p className="text-xs text-stone truncate">{displaySecondary}</p>
|
|
48
|
-
)}
|
|
49
|
-
</div>
|
|
50
|
-
</div>
|
|
51
|
-
|
|
52
|
-
{/* Icon */}
|
|
53
|
-
<div className="flex-shrink-0">
|
|
54
|
-
{startingChatWithId === participant.id ? (
|
|
55
|
-
<SpinnerGapIcon className="h-5 w-5 text-primary animate-spin" />
|
|
56
|
-
) : (
|
|
57
|
-
<ChatCircleDotsIcon className="h-5 w-5 text-stone" />
|
|
58
|
-
)}
|
|
59
|
-
</div>
|
|
60
|
-
</div>
|
|
61
|
-
</button>
|
|
62
|
-
</li>
|
|
63
|
-
)
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import type { Meta, StoryFn } from '@storybook/react'
|
|
2
|
-
import React from 'react'
|
|
3
|
-
|
|
4
|
-
import { mockParticipantSource } from '../../stories/mocks'
|
|
5
|
-
|
|
6
|
-
import { ParticipantPicker } from './index'
|
|
7
|
-
|
|
8
|
-
type ComponentProps = React.ComponentProps<typeof ParticipantPicker>
|
|
9
|
-
|
|
10
|
-
const meta: Meta<ComponentProps> = {
|
|
11
|
-
title: 'ParticipantPicker',
|
|
12
|
-
component: ParticipantPicker,
|
|
13
|
-
parameters: {
|
|
14
|
-
layout: 'fullscreen',
|
|
15
|
-
},
|
|
16
|
-
}
|
|
17
|
-
export default meta
|
|
18
|
-
|
|
19
|
-
const Template: StoryFn<ComponentProps> = (args) => {
|
|
20
|
-
return (
|
|
21
|
-
<div className="h-screen w-full">
|
|
22
|
-
<ParticipantPicker {...args} />
|
|
23
|
-
</div>
|
|
24
|
-
)
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export const Default: StoryFn<ComponentProps> = Template.bind({})
|
|
28
|
-
Default.args = {
|
|
29
|
-
participantSource: mockParticipantSource,
|
|
30
|
-
participantLabel: 'participants',
|
|
31
|
-
searchPlaceholder: 'Search participants...',
|
|
32
|
-
onClose: () => console.log('Close clicked'),
|
|
33
|
-
onSelectParticipant: (participant) => console.log('Selected:', participant),
|
|
34
|
-
existingParticipantIds: new Set(),
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export const WithExistingParticipants: StoryFn<ComponentProps> = Template.bind({})
|
|
38
|
-
WithExistingParticipants.args = {
|
|
39
|
-
participantSource: mockParticipantSource,
|
|
40
|
-
participantLabel: 'participants',
|
|
41
|
-
searchPlaceholder: 'Search participants...',
|
|
42
|
-
onClose: () => console.log('Close clicked'),
|
|
43
|
-
onSelectParticipant: (participant) => console.log('Selected:', participant),
|
|
44
|
-
existingParticipantIds: new Set(['participant-1', 'participant-3']),
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export const CustomLabels: StoryFn<ComponentProps> = Template.bind({})
|
|
48
|
-
CustomLabels.args = {
|
|
49
|
-
participantSource: mockParticipantSource,
|
|
50
|
-
participantLabel: 'followers',
|
|
51
|
-
searchPlaceholder: 'Search followers...',
|
|
52
|
-
onClose: () => console.log('Close clicked'),
|
|
53
|
-
onSelectParticipant: (participant) => console.log('Selected:', participant),
|
|
54
|
-
existingParticipantIds: new Set(),
|
|
55
|
-
}
|
|
56
|
-
|
|
@@ -1,229 +0,0 @@
|
|
|
1
|
-
import { ChatCircleDotsIcon } 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 { CloseButton } from '../CloseButton'
|
|
8
|
-
import Loading from '../Loading'
|
|
9
|
-
import { SearchInput } from '../SearchInput'
|
|
10
|
-
|
|
11
|
-
import { ParticipantItem } from './ParticipantItem'
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Generic participant picker component for starting conversations
|
|
15
|
-
*/
|
|
16
|
-
export const ParticipantPicker: React.FC<ParticipantPickerProps> = ({
|
|
17
|
-
participantSource,
|
|
18
|
-
onSelectParticipant,
|
|
19
|
-
onClose,
|
|
20
|
-
existingParticipantIds = new Set(),
|
|
21
|
-
participantLabel = 'participants',
|
|
22
|
-
searchPlaceholder = 'Search participants...',
|
|
23
|
-
className,
|
|
24
|
-
}) => {
|
|
25
|
-
const { debug } = useMessagingContext()
|
|
26
|
-
const [searchQuery, setSearchQuery] = useState('')
|
|
27
|
-
const [participants, setParticipants] = useState<Participant[]>([])
|
|
28
|
-
const [loading, setLoading] = useState(false)
|
|
29
|
-
const [error, setError] = useState<string | null>(null)
|
|
30
|
-
const [startingChatWithId, setStartingChatWithId] = useState<string | null>(
|
|
31
|
-
null
|
|
32
|
-
)
|
|
33
|
-
|
|
34
|
-
// Track if we've already loaded participants to prevent repeated loading
|
|
35
|
-
const loadedRef = useRef(false)
|
|
36
|
-
|
|
37
|
-
// Load participants initially - wait for participantSource to finish loading first
|
|
38
|
-
useEffect(() => {
|
|
39
|
-
// Wait for the participantSource to finish loading before we try to load participants
|
|
40
|
-
if (participantSource.loading) {
|
|
41
|
-
if (debug) {
|
|
42
|
-
console.log(
|
|
43
|
-
'[ParticipantPicker] Waiting for participant source to finish loading...'
|
|
44
|
-
)
|
|
45
|
-
}
|
|
46
|
-
return
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (loadedRef.current) return // Prevent multiple loads
|
|
50
|
-
|
|
51
|
-
const loadInitialParticipants = async () => {
|
|
52
|
-
if (debug) {
|
|
53
|
-
console.log('[ParticipantPicker] Loading initial participants...')
|
|
54
|
-
}
|
|
55
|
-
setLoading(true)
|
|
56
|
-
setError(null)
|
|
57
|
-
|
|
58
|
-
try {
|
|
59
|
-
const result = await participantSource.loadParticipants({
|
|
60
|
-
search: '', // Load all participants initially
|
|
61
|
-
limit: 100,
|
|
62
|
-
})
|
|
63
|
-
setParticipants(result.participants)
|
|
64
|
-
loadedRef.current = true // Mark as loaded
|
|
65
|
-
if (debug) {
|
|
66
|
-
console.log(
|
|
67
|
-
'[ParticipantPicker] Participants loaded successfully:',
|
|
68
|
-
result.participants.length
|
|
69
|
-
)
|
|
70
|
-
}
|
|
71
|
-
} catch (err) {
|
|
72
|
-
const errorMessage =
|
|
73
|
-
err instanceof Error ? err.message : 'Failed to load participants'
|
|
74
|
-
setError(errorMessage)
|
|
75
|
-
console.error('[ParticipantPicker] Failed to load participants:', err)
|
|
76
|
-
// Don't mark as loaded on error, allow retry
|
|
77
|
-
} finally {
|
|
78
|
-
setLoading(false)
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
loadInitialParticipants()
|
|
83
|
-
}, [participantSource, debug]) // Re-run when participantSource or debug changes
|
|
84
|
-
|
|
85
|
-
// Filter participants by search query and existing participants
|
|
86
|
-
const availableParticipants = participants
|
|
87
|
-
.filter((participant) => !existingParticipantIds.has(participant.id))
|
|
88
|
-
.filter((participant) => {
|
|
89
|
-
if (!searchQuery) return true
|
|
90
|
-
const searchLower = searchQuery.toLowerCase()
|
|
91
|
-
return (
|
|
92
|
-
participant.name.toLowerCase().includes(searchLower) ||
|
|
93
|
-
participant.email?.toLowerCase().includes(searchLower) ||
|
|
94
|
-
false
|
|
95
|
-
)
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
const handleSelectParticipant = useCallback(
|
|
99
|
-
async (participant: Participant) => {
|
|
100
|
-
if (startingChatWithId) return // Prevent multiple clicks
|
|
101
|
-
|
|
102
|
-
setStartingChatWithId(participant.id)
|
|
103
|
-
try {
|
|
104
|
-
await onSelectParticipant(participant)
|
|
105
|
-
} catch (error) {
|
|
106
|
-
console.error('[ParticipantPicker] Failed to start chat:', error)
|
|
107
|
-
// Reset the loading state on error
|
|
108
|
-
setStartingChatWithId(null)
|
|
109
|
-
}
|
|
110
|
-
// Note: Don't reset startingChatWithId on success because the dialog will close
|
|
111
|
-
},
|
|
112
|
-
[onSelectParticipant, startingChatWithId]
|
|
113
|
-
)
|
|
114
|
-
|
|
115
|
-
const handleKeyDown = (
|
|
116
|
-
event: React.KeyboardEvent,
|
|
117
|
-
participant: Participant
|
|
118
|
-
) => {
|
|
119
|
-
if (event.key === 'Enter' || event.key === ' ') {
|
|
120
|
-
event.preventDefault()
|
|
121
|
-
handleSelectParticipant(participant)
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
return (
|
|
126
|
-
<div
|
|
127
|
-
className={classNames(
|
|
128
|
-
'messaging-participant-picker flex flex-col h-full',
|
|
129
|
-
className
|
|
130
|
-
)}
|
|
131
|
-
>
|
|
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
|
-
<ParticipantItem
|
|
205
|
-
key={participant.id}
|
|
206
|
-
participant={participant}
|
|
207
|
-
handleSelectParticipant={handleSelectParticipant}
|
|
208
|
-
handleKeyDown={handleKeyDown}
|
|
209
|
-
displayName={displayName}
|
|
210
|
-
displaySecondary={displaySecondary}
|
|
211
|
-
/>
|
|
212
|
-
)
|
|
213
|
-
})}
|
|
214
|
-
|
|
215
|
-
{/* Loading indicator */}
|
|
216
|
-
{loading && (
|
|
217
|
-
<li className="p-4 flex justify-center">
|
|
218
|
-
<div className="flex items-center space-x-2">
|
|
219
|
-
<Loading className="w-6 h-6" />
|
|
220
|
-
<span className="text-sm text-stone">Loading more...</span>
|
|
221
|
-
</div>
|
|
222
|
-
</li>
|
|
223
|
-
)}
|
|
224
|
-
</ul>
|
|
225
|
-
)}
|
|
226
|
-
</div>
|
|
227
|
-
</div>
|
|
228
|
-
)
|
|
229
|
-
}
|