@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.
@@ -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
- }