@linktr.ee/messaging-react 1.3.0 → 1.4.0-rc-1761046174

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,33 +1,31 @@
1
- import React from 'react';
1
+ import React from 'react'
2
2
 
3
3
  /**
4
- * Error state component
4
+ * Error state component shown when something goes wrong
5
5
  */
6
- export const ErrorState: React.FC<{ error: string; onRetry?: () => void }> = ({ error, onRetry }) => (
7
- <div className="flex items-center justify-center h-full p-8">
8
- <div className="text-center max-w-md">
9
- <div className="w-24 h-24 bg-danger-alt rounded-full flex items-center justify-center mx-auto mb-6">
6
+ export const ErrorState: React.FC<{
7
+ message: string
8
+ onBack?: () => void
9
+ }> = ({ message, onBack }) => (
10
+ <div className="messaging-error-state flex items-center justify-center h-full p-8">
11
+ <div className="text-center max-w-sm">
12
+ <div className="w-24 h-24 bg-danger-alt/20 rounded-full flex items-center justify-center mx-auto mb-6">
10
13
  <span className="text-4xl">⚠️</span>
11
14
  </div>
12
-
13
- <h2 className="text-xl font-semibold text-charcoal mb-3">
14
- Connection Error
15
- </h2>
16
-
17
- <p className="text-stone text-sm mb-6">
18
- {error}
19
- </p>
20
15
 
21
- {onRetry && (
16
+ <h2 className="font-semibold text-charcoal mb-2">Oops!</h2>
17
+
18
+ <p className="text-stone text-sm mb-6">{message}</p>
19
+
20
+ {onBack && (
22
21
  <button
23
22
  type="button"
24
- onClick={onRetry}
25
- className="inline-flex items-center px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary-alt focus:outline-none focus:ring-2 focus:ring-primary transition-colors"
23
+ onClick={onBack}
24
+ className="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-white bg-primary hover:bg-primary-alt rounded-lg focus:outline-none focus:ring-2 focus:ring-primary transition-colors"
26
25
  >
27
- Try Again
26
+ Go Back
28
27
  </button>
29
28
  )}
30
29
  </div>
31
30
  </div>
32
- );
33
-
31
+ )
@@ -4,7 +4,7 @@ import Loading from '../Loading'
4
4
  * Loading state component
5
5
  */
6
6
  export const LoadingState = () => (
7
- <div className="flex items-center justify-center h-full">
7
+ <div className="messaging-loading-state flex items-center justify-center h-full">
8
8
  <div className="flex items-center">
9
9
  <Loading className="w-6 h-6" />
10
10
  <span className="text-sm text-stone">Loading messages</span>
@@ -21,6 +21,7 @@ export const MessagingShell: React.FC<MessagingShellProps> = ({
21
21
  renderMessageInputActions,
22
22
  onChannelSelect,
23
23
  onParticipantSelect,
24
+ initialParticipantFilter,
24
25
  }) => {
25
26
  const {
26
27
  service,
@@ -39,6 +40,10 @@ export const MessagingShell: React.FC<MessagingShellProps> = ({
39
40
  Set<string>
40
41
  >(new Set())
41
42
  const [pickerKey, setPickerKey] = useState(0) // Key to force remount of ParticipantPicker
43
+ const [directConversationMode, setDirectConversationMode] = useState(false)
44
+ const [directConversationError, setDirectConversationError] = useState<
45
+ string | null
46
+ >(null)
42
47
 
43
48
  const participantPickerRef = useRef<HTMLDialogElement>(null)
44
49
 
@@ -112,6 +117,64 @@ export const MessagingShell: React.FC<MessagingShellProps> = ({
112
117
  syncChannels()
113
118
  }, [client, isConnected, syncChannels])
114
119
 
120
+ // Load initial channel for direct conversation mode
121
+ useEffect(() => {
122
+ if (!initialParticipantFilter || !client || !isConnected) return
123
+
124
+ const loadInitialChannel = async () => {
125
+ const userId = client.userID
126
+ if (!userId) return
127
+
128
+ try {
129
+ if (debug) {
130
+ console.log(
131
+ '[MessagingShell] Loading initial conversation with:',
132
+ initialParticipantFilter
133
+ )
134
+ }
135
+
136
+ const channels = await client.queryChannels(
137
+ {
138
+ type: 'messaging',
139
+ members: { $in: [userId, initialParticipantFilter] },
140
+ },
141
+ {},
142
+ { limit: 1 }
143
+ )
144
+
145
+ if (channels.length > 0) {
146
+ setSelectedChannel(channels[0])
147
+ setDirectConversationMode(true)
148
+ setDirectConversationError(null)
149
+
150
+ if (debug) {
151
+ console.log(
152
+ '[MessagingShell] Initial conversation loaded:',
153
+ channels[0].id
154
+ )
155
+ }
156
+ } else {
157
+ setDirectConversationError('No conversation found with this account')
158
+
159
+ if (debug) {
160
+ console.log(
161
+ '[MessagingShell] No conversation found for:',
162
+ initialParticipantFilter
163
+ )
164
+ }
165
+ }
166
+ } catch (err) {
167
+ console.error(
168
+ '[MessagingShell] Failed to load initial conversation:',
169
+ err
170
+ )
171
+ setDirectConversationError('Failed to load conversation')
172
+ }
173
+ }
174
+
175
+ loadInitialChannel()
176
+ }, [initialParticipantFilter, client, isConnected, debug])
177
+
115
178
  const handleChannelSelect = useCallback(
116
179
  (channel: Channel) => {
117
180
  setSelectedChannel(channel)
@@ -121,8 +184,12 @@ export const MessagingShell: React.FC<MessagingShellProps> = ({
121
184
  )
122
185
 
123
186
  const handleBackToChannelList = useCallback(() => {
187
+ // In direct conversation mode, don't allow going back to channel list
188
+ // The parent component should handle navigation
189
+ if (directConversationMode) return
190
+
124
191
  setSelectedChannel(null)
125
- }, [])
192
+ }, [directConversationMode])
126
193
 
127
194
  const handleStartConversation = useCallback(() => {
128
195
  if (participantSource) {
@@ -218,7 +285,7 @@ export const MessagingShell: React.FC<MessagingShellProps> = ({
218
285
  if (error) {
219
286
  return (
220
287
  <div className={classNames('h-full', className)}>
221
- <ErrorState error={error} onRetry={refreshConnection} />
288
+ <ErrorState message={error} onBack={refreshConnection} />
222
289
  </div>
223
290
  )
224
291
  }
@@ -228,24 +295,43 @@ export const MessagingShell: React.FC<MessagingShellProps> = ({
228
295
  return (
229
296
  <div className={classNames('h-full', className)}>
230
297
  <ErrorState
231
- error="Not connected to messaging service"
232
- onRetry={refreshConnection}
298
+ message="Not connected to messaging service"
299
+ onBack={refreshConnection}
233
300
  />
234
301
  </div>
235
302
  )
236
303
  }
237
304
 
305
+ // Show direct conversation error state
306
+ if (directConversationError) {
307
+ return (
308
+ <div className={classNames('h-full', className)}>
309
+ <ErrorState message={directConversationError} />
310
+ </div>
311
+ )
312
+ }
313
+
238
314
  return (
239
- <div className={classNames('h-full bg-white overflow-hidden', className)}>
315
+ <div
316
+ className={classNames(
317
+ 'messaging-shell h-full bg-white overflow-hidden',
318
+ className
319
+ )}
320
+ >
240
321
  <div className="flex h-full min-h-0">
241
322
  {/* Channel List Sidebar */}
242
323
  <div
243
324
  className={classNames(
244
- 'min-h-0 min-w-0 bg-white lg:bg-chalk lg:flex lg:flex-col lg:border-r lg:border-sand',
325
+ 'messaging-channel-list-sidebar min-h-0 min-w-0 bg-white lg:bg-chalk lg:flex lg:flex-col lg:border-r lg:border-sand',
245
326
  {
327
+ // In direct conversation mode, always hide the channel list
328
+ hidden: directConversationMode,
329
+ // Normal mode: hide on mobile when channel selected, show on desktop
246
330
  'hidden lg:flex lg:w-80 lg:min-w-[280px] lg:max-w-[360px]':
247
- isChannelSelected,
248
- 'flex flex-col w-full lg:flex-1 lg:max-w-2xl': !isChannelSelected,
331
+ !directConversationMode && isChannelSelected,
332
+ // Normal mode: show when no channel selected
333
+ 'flex flex-col w-full lg:flex-1 lg:max-w-2xl':
334
+ !directConversationMode && !isChannelSelected,
249
335
  }
250
336
  )}
251
337
  >
@@ -262,10 +348,15 @@ export const MessagingShell: React.FC<MessagingShellProps> = ({
262
348
 
263
349
  {/* Channel View */}
264
350
  <div
265
- className={classNames('flex-1 flex-col min-w-0 min-h-0', {
266
- 'hidden lg:flex': !isChannelSelected,
267
- flex: isChannelSelected,
268
- })}
351
+ className={classNames(
352
+ 'messaging-conversation-view flex-1 flex-col min-w-0 min-h-0',
353
+ {
354
+ // In direct conversation mode, always show (full width)
355
+ flex: directConversationMode || isChannelSelected,
356
+ // Normal mode: hide on mobile when no channel selected
357
+ 'hidden lg:flex': !directConversationMode && !isChannelSelected,
358
+ }
359
+ )}
269
360
  >
270
361
  {selectedChannel ? (
271
362
  <div className="flex-1 min-h-0 flex flex-col">
@@ -123,7 +123,12 @@ export const ParticipantPicker: React.FC<ParticipantPickerProps> = ({
123
123
  }
124
124
 
125
125
  return (
126
- <div className={classNames('flex flex-col h-full', className)}>
126
+ <div
127
+ className={classNames(
128
+ 'messaging-participant-picker flex flex-col h-full',
129
+ className
130
+ )}
131
+ >
127
132
  {/* Header */}
128
133
  <div className="px-4 py-4 border-b border-sand bg-chalk">
129
134
  <div className="flex items-center justify-between mb-3">
package/src/types.ts CHANGED
@@ -59,6 +59,16 @@ export interface MessagingShellProps {
59
59
  renderMessageInputActions?: (channel: Channel) => React.ReactNode;
60
60
  onChannelSelect?: (channel: Channel) => void;
61
61
  onParticipantSelect?: (participant: Participant) => void;
62
+
63
+ /**
64
+ * Auto-select a conversation with this participant on mount.
65
+ * Useful for deep-linking to a specific conversation (e.g., /messages/[accountUuid])
66
+ *
67
+ * If a channel with this participant exists, it will be auto-selected.
68
+ * If no channel exists and participantSource is provided,
69
+ * the participant picker can be shown to create a new conversation.
70
+ */
71
+ initialParticipantFilter?: string;
62
72
  }
63
73
 
64
74
  /**