@linktr.ee/messaging-react 1.24.4 → 1.25.0-rc-1774238116

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,19 +1,11 @@
1
1
  import {
2
2
  ArrowLeftIcon,
3
3
  DotsThreeIcon,
4
- FlagIcon,
5
- ProhibitInsetIcon,
6
- SignOutIcon,
7
- SpinnerGapIcon,
8
4
  StarIcon,
9
5
  } from '@phosphor-icons/react'
10
6
  import classNames from 'classnames'
11
7
  import React, { useState, useCallback, useRef, useEffect } from 'react'
12
- import {
13
- Channel as ChannelType,
14
- ChannelMemberResponse,
15
- Event,
16
- } from 'stream-chat'
8
+ import { Channel as ChannelType, Event } from 'stream-chat'
17
9
  import {
18
10
  Channel,
19
11
  Window,
@@ -24,12 +16,10 @@ import {
24
16
  MessageUIComponentProps,
25
17
  } from 'stream-chat-react'
26
18
 
27
- import { useMessagingContext } from '../providers/MessagingProvider'
28
19
  import type { ChannelViewProps } from '../types'
29
20
 
30
- import ActionButton from './ActionButton'
31
21
  import { Avatar } from './Avatar'
32
- import { CloseButton } from './CloseButton'
22
+ import { ChannelInfoDialog } from './ChannelInfoDialog'
33
23
  import { CustomDateSeparator } from './CustomDateSeparator'
34
24
  import { CustomMessage } from './CustomMessage'
35
25
  import { CustomMessageInput } from './CustomMessageInput'
@@ -37,17 +27,6 @@ import { CustomSystemMessage } from './CustomSystemMessage'
37
27
  import { ChannelEmptyState } from './MessagingShell/ChannelEmptyState'
38
28
  import { LoadingState } from './MessagingShell/LoadingState'
39
29
 
40
- // Custom user type with email and username
41
- type CustomUser = {
42
- email?: string
43
- username?: string
44
- }
45
-
46
- // Blocked user from Stream Chat API
47
- type BlockedUser = {
48
- blocked_user_id: string
49
- }
50
-
51
30
  const ICON_BTN_CLASS =
52
31
  'size-10 rounded-full bg-[#F1F0EE] hover:bg-[#E5E4E1] flex items-center justify-center transition-colors duration-150 focus-ring'
53
32
 
@@ -231,292 +210,6 @@ const CustomChannelHeader: React.FC<{
231
210
  )
232
211
  }
233
212
 
234
- /**
235
- * Channel info dialog (matching original implementation)
236
- */
237
- const ChannelInfoDialog: React.FC<{
238
- dialogRef: React.RefObject<HTMLDialogElement>
239
- onClose: () => void
240
- participant: ChannelMemberResponse | undefined
241
- channel: ChannelType
242
- followerStatusLabel?: string
243
- onLeaveConversation?: (channel: ChannelType) => void
244
- onBlockParticipant?: (participantId?: string) => void
245
- showDeleteConversation?: boolean
246
- onDeleteConversationClick?: () => void
247
- onBlockParticipantClick?: () => void
248
- onReportParticipantClick?: () => void
249
- customChannelActions?: React.ReactNode
250
- }> = ({
251
- dialogRef,
252
- onClose,
253
- participant,
254
- channel,
255
- followerStatusLabel,
256
- onLeaveConversation,
257
- onBlockParticipant,
258
- showDeleteConversation = true,
259
- onDeleteConversationClick,
260
- onBlockParticipantClick,
261
- onReportParticipantClick,
262
- customChannelActions,
263
- }) => {
264
- const { service, debug } = useMessagingContext()
265
- const [isParticipantBlocked, setIsParticipantBlocked] = useState(false)
266
- const [isLeaving, setIsLeaving] = useState(false)
267
- const [isUpdatingBlockStatus, setIsUpdatingBlockStatus] = useState(false)
268
-
269
- // Check if participant is blocked when participant changes
270
- const checkIsParticipantBlocked = useCallback(async () => {
271
- if (!service || !participant?.user?.id) return
272
-
273
- try {
274
- const blockedUsers = await service.getBlockedUsers()
275
- const isBlocked = blockedUsers.some(
276
- (user: BlockedUser) => user.blocked_user_id === participant?.user?.id
277
- )
278
- setIsParticipantBlocked(isBlocked)
279
- } catch (error) {
280
- console.error(
281
- '[ChannelInfoDialog] Failed to check blocked status:',
282
- error
283
- )
284
- }
285
- }, [service, participant?.user?.id])
286
-
287
- useEffect(() => {
288
- checkIsParticipantBlocked()
289
- }, [checkIsParticipantBlocked])
290
-
291
- const handleLeaveConversation = async () => {
292
- if (isLeaving) return
293
-
294
- // Fire analytics callback before action
295
- onDeleteConversationClick?.()
296
-
297
- if (debug) {
298
- console.log('[ChannelInfoDialog] Leave conversation', channel.cid)
299
- }
300
- setIsLeaving(true)
301
-
302
- try {
303
- const actingUserId = channel._client?.userID ?? null
304
- await channel.hide(actingUserId, false)
305
-
306
- if (onLeaveConversation) {
307
- await onLeaveConversation(channel)
308
- }
309
-
310
- onClose()
311
- } catch (error) {
312
- console.error('[ChannelInfoDialog] Failed to leave conversation', error)
313
- } finally {
314
- setIsLeaving(false)
315
- }
316
- }
317
-
318
- const handleBlockUser = async () => {
319
- if (isUpdatingBlockStatus || !service) return
320
-
321
- // Fire analytics callback before action
322
- onBlockParticipantClick?.()
323
-
324
- if (debug) {
325
- console.log('[ChannelInfoDialog] Block member', participant?.user?.id)
326
- }
327
- setIsUpdatingBlockStatus(true)
328
-
329
- try {
330
- await service.blockUser(participant?.user?.id)
331
-
332
- if (onBlockParticipant) {
333
- await onBlockParticipant(participant?.user?.id)
334
- }
335
-
336
- onClose()
337
- } catch (error) {
338
- console.error('[ChannelInfoDialog] Failed to block member', error)
339
- } finally {
340
- setIsUpdatingBlockStatus(false)
341
- }
342
- }
343
-
344
- const handleUnblockUser = async () => {
345
- if (isUpdatingBlockStatus || !service) return
346
-
347
- // Fire analytics callback before action
348
- onBlockParticipantClick?.()
349
-
350
- if (debug) {
351
- console.log('[ChannelInfoDialog] Unblock member', participant?.user?.id)
352
- }
353
- setIsUpdatingBlockStatus(true)
354
-
355
- try {
356
- await service.unBlockUser(participant?.user?.id)
357
-
358
- if (onBlockParticipant) {
359
- await onBlockParticipant(participant?.user?.id)
360
- }
361
-
362
- onClose()
363
- } catch (error) {
364
- console.error('[ChannelInfoDialog] Failed to unblock member', error)
365
- } finally {
366
- setIsUpdatingBlockStatus(false)
367
- }
368
- }
369
-
370
- const handleReportUser = () => {
371
- // Fire analytics callback before action
372
- onReportParticipantClick?.()
373
-
374
- onClose()
375
- window.open(
376
- 'https://linktr.ee/s/about/trust-center/report',
377
- '_blank',
378
- 'noopener,noreferrer'
379
- )
380
- }
381
-
382
- if (!participant) return null
383
-
384
- const participantName =
385
- participant.user?.name || participant.user?.id || 'Unknown member'
386
- const participantImage = participant.user?.image
387
- const participantEmail = (participant.user as CustomUser)?.email
388
- const participantUsername = (participant.user as CustomUser)?.username
389
- const participantSecondary = participantEmail
390
- ? participantEmail
391
- : participantUsername
392
- ? `linktr.ee/${participantUsername}`
393
- : undefined
394
- const participantId = participant.user?.id || 'unknown'
395
-
396
- return (
397
- // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions
398
- <dialog
399
- ref={dialogRef}
400
- className="mes-dialog group"
401
- onClose={onClose}
402
- onClick={(e) => {
403
- if (e.target === dialogRef.current) {
404
- onClose()
405
- }
406
- }}
407
- >
408
- <div className="ml-auto flex h-full w-full flex-col bg-white shadow-none transition-shadow duration-200 group-open:shadow-max-elevation-light">
409
- <div className="flex items-center justify-between border-b border-sand px-4 py-3">
410
- <h2 className="text-base font-semibold text-charcoal">Chat info</h2>
411
- <CloseButton onClick={onClose} />
412
- </div>
413
-
414
- <div className="flex-1 px-2 overflow-y-auto w-full">
415
- <div
416
- className="flex flex-col items-center gap-3 self-stretch px-4 py-2 mt-6 rounded-lg border border-black/[0.04]"
417
- style={{ backgroundColor: '#FBFAF9' }}
418
- >
419
- <div className="flex items-center gap-3 w-full">
420
- <Avatar
421
- id={participantId}
422
- name={participantName}
423
- image={participantImage}
424
- size={88}
425
- shape="circle"
426
- />
427
- <div className="flex flex-col min-w-0 flex-1">
428
- <p className="truncate text-base font-semibold text-charcoal">
429
- {participantName}
430
- </p>
431
- {participantSecondary && (
432
- <p className="truncate text-sm text-[#00000055]">
433
- {participantSecondary}
434
- </p>
435
- )}
436
- {followerStatusLabel && (
437
- <span
438
- className="mt-1 rounded-full text-xs font-normal w-fit"
439
- style={{
440
- padding: '4px 8px',
441
- backgroundColor:
442
- followerStatusLabel === 'Subscribed to you'
443
- ? '#DCFCE7'
444
- : '#F5F5F4',
445
- color:
446
- followerStatusLabel === 'Subscribed to you'
447
- ? '#008236'
448
- : '#78716C',
449
- lineHeight: '133.333%',
450
- letterSpacing: '0.21px',
451
- }}
452
- >
453
- {followerStatusLabel}
454
- </span>
455
- )}
456
- </div>
457
- </div>
458
- </div>
459
-
460
- <ul className="flex flex-col gap-2 mt-2">
461
- {showDeleteConversation && (
462
- <li>
463
- <ActionButton
464
- onClick={handleLeaveConversation}
465
- disabled={isLeaving}
466
- aria-busy={isLeaving}
467
- >
468
- {isLeaving ? (
469
- <SpinnerGapIcon className="h-5 w-5 animate-spin" />
470
- ) : (
471
- <SignOutIcon className="h-5 w-5" />
472
- )}
473
- <span>Delete Conversation</span>
474
- </ActionButton>
475
- </li>
476
- )}
477
- <li>
478
- {isParticipantBlocked ? (
479
- <ActionButton
480
- onClick={handleUnblockUser}
481
- disabled={isUpdatingBlockStatus}
482
- aria-busy={isUpdatingBlockStatus}
483
- >
484
- {isUpdatingBlockStatus ? (
485
- <SpinnerGapIcon className="h-5 w-5 animate-spin" />
486
- ) : (
487
- <ProhibitInsetIcon className="h-5 w-5" />
488
- )}
489
- <span>Unblock</span>
490
- </ActionButton>
491
- ) : (
492
- <ActionButton
493
- onClick={handleBlockUser}
494
- disabled={isUpdatingBlockStatus}
495
- aria-busy={isUpdatingBlockStatus}
496
- >
497
- {isUpdatingBlockStatus ? (
498
- <SpinnerGapIcon className="h-5 w-5 animate-spin" />
499
- ) : (
500
- <ProhibitInsetIcon className="h-5 w-5" />
501
- )}
502
- <span>Block</span>
503
- </ActionButton>
504
- )}
505
- </li>
506
- <li>
507
- <ActionButton variant="danger" onClick={handleReportUser}>
508
- <FlagIcon className="h-5 w-5" />
509
- <span>Report</span>
510
- </ActionButton>
511
- </li>
512
- {customChannelActions}
513
- </ul>
514
- </div>
515
- </div>
516
- </dialog>
517
- )
518
- }
519
-
520
213
  /**
521
214
  * Inner component that has access to channel context
522
215
  */
@@ -535,6 +228,7 @@ const ChannelViewInner: React.FC<{
535
228
  showStarButton?: boolean
536
229
  chatbotVotingEnabled?: boolean
537
230
  renderChannelBanner?: () => React.ReactNode
231
+ customProfileContent?: React.ReactNode
538
232
  customChannelActions?: React.ReactNode
539
233
  renderMessage?: (
540
234
  messageNode: React.ReactElement,
@@ -554,6 +248,7 @@ const ChannelViewInner: React.FC<{
554
248
  showStarButton = false,
555
249
  chatbotVotingEnabled = false,
556
250
  renderChannelBanner,
251
+ customProfileContent,
557
252
  customChannelActions,
558
253
  renderMessage,
559
254
  }) => {
@@ -664,6 +359,7 @@ const ChannelViewInner: React.FC<{
664
359
  onDeleteConversationClick={onDeleteConversationClick}
665
360
  onBlockParticipantClick={onBlockParticipantClick}
666
361
  onReportParticipantClick={onReportParticipantClick}
362
+ customProfileContent={customProfileContent}
667
363
  customChannelActions={customChannelActions}
668
364
  />
669
365
  </>
@@ -694,6 +390,7 @@ export const ChannelView = React.memo<ChannelViewProps>(
694
390
  showStarButton = false,
695
391
  chatbotVotingEnabled = false,
696
392
  renderChannelBanner,
393
+ customProfileContent,
697
394
  customChannelActions,
698
395
  renderMessage,
699
396
  }) => {
@@ -771,6 +468,7 @@ export const ChannelView = React.memo<ChannelViewProps>(
771
468
  showStarButton={showStarButton}
772
469
  chatbotVotingEnabled={chatbotVotingEnabled}
773
470
  renderChannelBanner={renderChannelBanner}
471
+ customProfileContent={customProfileContent}
774
472
  customChannelActions={customChannelActions}
775
473
  renderMessage={renderMessage}
776
474
  />
@@ -38,6 +38,7 @@ export const MessagingShell: React.FC<MessagingShellProps> = ({
38
38
  chatbotVotingEnabled = false,
39
39
  renderMessagePreview,
40
40
  renderChannelBanner,
41
+ customProfileContent,
41
42
  customChannelActions,
42
43
  renderMessage,
43
44
  }) => {
@@ -499,6 +500,7 @@ export const MessagingShell: React.FC<MessagingShellProps> = ({
499
500
  onMessageSent={onMessageSent}
500
501
  showStarButton={showStarButton}
501
502
  chatbotVotingEnabled={chatbotVotingEnabled}
503
+ customProfileContent={customProfileContent}
502
504
  customChannelActions={customChannelActions}
503
505
  renderMessage={renderMessage}
504
506
  />
package/src/types.ts CHANGED
@@ -185,6 +185,16 @@ export interface ChannelViewProps {
185
185
  */
186
186
  renderChannelBanner?: () => React.ReactNode
187
187
 
188
+ /**
189
+ * Custom content rendered below the participant name and contact details
190
+ * in the channel info dialog profile card.
191
+ * Useful for badges (e.g. follower status), metadata, or any extra info.
192
+ *
193
+ * @example
194
+ * customProfileContent={<SubscriptionBadge isFollower={channel.data?.isFollower} />}
195
+ */
196
+ customProfileContent?: React.ReactNode
197
+
188
198
  /**
189
199
  * Custom actions rendered at the bottom of the channel info dialog
190
200
  * (below Delete Conversation, Block/Unblock, Report).
@@ -227,6 +237,7 @@ export type ChannelViewPassthroughProps = Pick<
227
237
  | 'showStarButton'
228
238
  | 'chatbotVotingEnabled'
229
239
  | 'renderChannelBanner'
240
+ | 'customProfileContent'
230
241
  | 'customChannelActions'
231
242
  | 'renderMessage'
232
243
  >