@linktr.ee/messaging-react 3.1.4-rc-1780636753 → 3.3.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.
Files changed (41) hide show
  1. package/README.md +1 -18
  2. package/dist/{Card-jyXjZZ0u.js → Card-BAc2cgtn.js} +3 -3
  3. package/dist/{Card-jyXjZZ0u.js.map → Card-BAc2cgtn.js.map} +1 -1
  4. package/dist/{Card-D_XOj1eE.cjs → Card-Cn1cBVnr.cjs} +2 -2
  5. package/dist/{Card-D_XOj1eE.cjs.map → Card-Cn1cBVnr.cjs.map} +1 -1
  6. package/dist/{Card-BkgsPkp4.cjs → Card-DAyszUxa.cjs} +2 -2
  7. package/dist/{Card-BkgsPkp4.cjs.map → Card-DAyszUxa.cjs.map} +1 -1
  8. package/dist/{Card-BwFdJXYm.js → Card-D_2VQScd.js} +2 -2
  9. package/dist/{Card-BwFdJXYm.js.map → Card-D_2VQScd.js.map} +1 -1
  10. package/dist/{Card-B9atg4sP.js → Card-D_G8133I.js} +2 -2
  11. package/dist/{Card-B9atg4sP.js.map → Card-D_G8133I.js.map} +1 -1
  12. package/dist/{Card-1U2tLPcp.cjs → Card-gYxPXe_W.cjs} +2 -2
  13. package/dist/{Card-1U2tLPcp.cjs.map → Card-gYxPXe_W.cjs.map} +1 -1
  14. package/dist/{LockedThumbnail-Dwt_goCX.js → LockedThumbnail-C7tWpOQr.js} +2 -2
  15. package/dist/{LockedThumbnail-Dwt_goCX.js.map → LockedThumbnail-C7tWpOQr.js.map} +1 -1
  16. package/dist/{LockedThumbnail-oxtdpgut.cjs → LockedThumbnail-DtOTZl3l.cjs} +2 -2
  17. package/dist/{LockedThumbnail-oxtdpgut.cjs.map → LockedThumbnail-DtOTZl3l.cjs.map} +1 -1
  18. package/dist/{index-CO975B6P.js → index-C_NFzAB9.js} +1228 -1289
  19. package/dist/index-C_NFzAB9.js.map +1 -0
  20. package/dist/index-_Se6ovQm.cjs +2 -0
  21. package/dist/index-_Se6ovQm.cjs.map +1 -0
  22. package/dist/index.cjs +1 -1
  23. package/dist/index.d.ts +24 -49
  24. package/dist/index.js +1 -1
  25. package/package.json +4 -3
  26. package/src/components/ChannelActionsMenu/ChannelActionsMenu.test.tsx +305 -0
  27. package/src/components/ChannelActionsMenu/index.tsx +221 -0
  28. package/src/components/ChannelList/index.test.tsx +3 -151
  29. package/src/components/ChannelList/index.tsx +4 -72
  30. package/src/components/ChannelView.stories.tsx +3 -73
  31. package/src/components/ChannelView.test.tsx +33 -29
  32. package/src/components/ChannelView.tsx +71 -109
  33. package/src/components/MessagingShell/index.tsx +2 -0
  34. package/src/hooks/useChannelModerationActions.ts +227 -0
  35. package/src/index.ts +0 -1
  36. package/src/types.ts +25 -48
  37. package/dist/index-CO975B6P.js.map +0 -1
  38. package/dist/index-D4Dse1Lu.cjs +0 -2
  39. package/dist/index-D4Dse1Lu.cjs.map +0 -1
  40. package/src/components/ChannelInfoDialog/ChannelInfoDialog.test.tsx +0 -333
  41. package/src/components/ChannelInfoDialog/index.tsx +0 -336
@@ -1,9 +1,4 @@
1
- import {
2
- ArrowLeftIcon,
3
- DotsThreeIcon,
4
- SparkleIcon,
5
- StarIcon,
6
- } from '@phosphor-icons/react'
1
+ import { ArrowLeftIcon, SparkleIcon, StarIcon } from '@phosphor-icons/react'
7
2
  import classNames from 'classnames'
8
3
  import React, { useCallback, useEffect, useRef } from 'react'
9
4
  import { Channel as ChannelType } from 'stream-chat'
@@ -22,7 +17,7 @@ import type { ChannelViewProps } from '../types'
22
17
  import { resolveParticipantDisplayName } from '../utils/resolveParticipantDisplayName'
23
18
 
24
19
  import { Avatar } from './Avatar'
25
- import { ChannelInfoDialog } from './ChannelInfoDialog'
20
+ import { ChannelActionsMenu } from './ChannelActionsMenu'
26
21
  import { CustomDateSeparator } from './CustomDateSeparator'
27
22
  import { CustomMessage } from './CustomMessage'
28
23
  import { CustomMessageActions } from './CustomMessage/CustomMessageActions'
@@ -44,17 +39,33 @@ const DM_AGENT_HEADER_HELPER_TEXT = 'Replies instantly with AI assistant'
44
39
  const CustomChannelHeader: React.FC<{
45
40
  onBack?: () => void
46
41
  showBackButton: boolean
47
- onShowInfo: () => void
48
- canShowInfo: boolean
49
42
  showStarButton?: boolean
50
43
  dmAgentEnabled?: boolean
44
+ onLeaveConversation?: (channel: ChannelType) => void
45
+ onBlockParticipant?: (participantId?: string) => void
46
+ showDeleteConversation?: boolean
47
+ showBlockParticipant?: boolean
48
+ showReportParticipant?: boolean
49
+ onDeleteConversationClick?: () => void
50
+ onBlockParticipantClick?: () => void
51
+ onReportParticipantClick?: () => void
52
+ customChannelActions?: React.ReactNode
53
+ showActionsMenu?: boolean
51
54
  }> = ({
52
55
  onBack,
53
56
  showBackButton,
54
- onShowInfo,
55
- canShowInfo,
56
57
  showStarButton = false,
57
58
  dmAgentEnabled = false,
59
+ onLeaveConversation,
60
+ onBlockParticipant,
61
+ showDeleteConversation = true,
62
+ showBlockParticipant = true,
63
+ showReportParticipant = true,
64
+ onDeleteConversationClick,
65
+ onBlockParticipantClick,
66
+ onReportParticipantClick,
67
+ customChannelActions,
68
+ showActionsMenu = true,
58
69
  }) => {
59
70
  const { channel } = useChannelStateContext()
60
71
 
@@ -143,14 +154,22 @@ const CustomChannelHeader: React.FC<{
143
154
  />
144
155
  </button>
145
156
  )}
146
- <button
147
- className={classNames(ICON_BTN_CLASS, 'bg-[#F1F0EE]')}
148
- onClick={onShowInfo}
149
- type="button"
150
- aria-label="Show info"
151
- >
152
- <DotsThreeIcon className="size-5 text-black/90" />
153
- </button>
157
+ {showActionsMenu && (
158
+ <ChannelActionsMenu
159
+ channel={channel}
160
+ participant={participant}
161
+ showDeleteConversation={showDeleteConversation}
162
+ showBlockParticipant={showBlockParticipant}
163
+ showReportParticipant={showReportParticipant}
164
+ onLeaveConversation={onLeaveConversation}
165
+ onBlockParticipant={onBlockParticipant}
166
+ onDeleteConversationClick={onDeleteConversationClick}
167
+ onBlockParticipantClick={onBlockParticipantClick}
168
+ onReportParticipantClick={onReportParticipantClick}
169
+ customChannelActions={customChannelActions}
170
+ triggerClassName={classNames(ICON_BTN_CLASS, 'bg-[#F1F0EE]')}
171
+ />
172
+ )}
154
173
  </div>
155
174
  </div>
156
175
  <div className="px-6 py-3 hidden @lg:flex items-center justify-between gap-3 min-h-12 border-b border-b-black/[0.08]">
@@ -178,20 +197,9 @@ const CustomChannelHeader: React.FC<{
178
197
  size={48}
179
198
  />
180
199
  <div className="min-w-0">
181
- {canShowInfo ? (
182
- <button
183
- type="button"
184
- onClick={onShowInfo}
185
- className="flex items-center gap-1 font-medium text-black/90 truncate hover:text-black/70 transition-colors"
186
- aria-label={`View info for ${participantName}`}
187
- >
188
- <span className="truncate">{participantName}</span>
189
- </button>
190
- ) : (
191
- <h1 className="font-medium text-black/90 truncate">
192
- {participantName}
193
- </h1>
194
- )}
200
+ <h1 className="font-medium text-black/90 truncate">
201
+ {participantName}
202
+ </h1>
195
203
  {dmAgentEnabled && (
196
204
  <div className="mt-0.5 flex items-center gap-1 text-[10px] leading-3 text-black/55">
197
205
  <SparkleIcon className="size-3 shrink-0 text-black/55" />
@@ -219,15 +227,21 @@ const CustomChannelHeader: React.FC<{
219
227
  />
220
228
  </button>
221
229
  )}
222
- {canShowInfo && onShowInfo && (
223
- <button
224
- className={ICON_BTN_CLASS}
225
- onClick={onShowInfo}
226
- type="button"
227
- aria-label="Show info"
228
- >
229
- <DotsThreeIcon className="size-6 text-black/90" />
230
- </button>
230
+ {showActionsMenu && (
231
+ <ChannelActionsMenu
232
+ channel={channel}
233
+ participant={participant}
234
+ showDeleteConversation={showDeleteConversation}
235
+ showBlockParticipant={showBlockParticipant}
236
+ showReportParticipant={showReportParticipant}
237
+ onLeaveConversation={onLeaveConversation}
238
+ onBlockParticipant={onBlockParticipant}
239
+ onDeleteConversationClick={onDeleteConversationClick}
240
+ onBlockParticipantClick={onBlockParticipantClick}
241
+ onReportParticipantClick={onReportParticipantClick}
242
+ customChannelActions={customChannelActions}
243
+ triggerClassName={ICON_BTN_CLASS}
244
+ />
231
245
  )}
232
246
  </div>
233
247
  </div>
@@ -253,13 +267,11 @@ const ChannelViewInner: React.FC<{
253
267
  onReportParticipantClick?: () => void
254
268
  showBlockParticipant?: boolean
255
269
  showReportParticipant?: boolean
256
- showFollowerStatus?: boolean
257
270
  composerDisabled?: boolean
258
271
  composerDisabledReason?: string
259
272
  showStarButton?: boolean
260
273
  chatbotVotingEnabled?: boolean
261
274
  renderChannelBanner?: () => React.ReactNode
262
- customProfileContent?: React.ReactNode
263
275
  customChannelActions?: React.ReactNode
264
276
  renderMessage?: (
265
277
  messageNode: React.ReactElement,
@@ -267,6 +279,7 @@ const ChannelViewInner: React.FC<{
267
279
  ) => React.ReactNode
268
280
  dmAgentEnabled?: boolean
269
281
  viewerLanguage?: string
282
+ showChannelInfo?: boolean
270
283
  }> = ({
271
284
  onBack,
272
285
  showBackButton,
@@ -281,22 +294,20 @@ const ChannelViewInner: React.FC<{
281
294
  onReportParticipantClick,
282
295
  showBlockParticipant = true,
283
296
  showReportParticipant = true,
284
- showFollowerStatus = true,
285
297
  composerDisabled = false,
286
298
  composerDisabledReason,
287
299
  showStarButton = false,
288
300
  chatbotVotingEnabled = false,
289
301
  renderChannelBanner,
290
- customProfileContent,
291
302
  customChannelActions,
292
303
  renderMessage,
293
304
  dmAgentEnabled = false,
294
305
  viewerLanguage,
306
+ showChannelInfo = true,
295
307
  }) => {
296
308
  const { channel } = useChannelStateContext()
297
- const infoDialogRef = useRef<HTMLDialogElement>(null)
298
309
 
299
- // Get participant info for info dialog
310
+ // Get participant info (the other channel member)
300
311
  const participant = React.useMemo(() => {
301
312
  const myUserId = channel._client?.userID
302
313
  if (!myUserId) return undefined
@@ -325,39 +336,6 @@ const ChannelViewInner: React.FC<{
325
336
  currentUserIsAccount === false &&
326
337
  participantIsAccount === true
327
338
 
328
- // Get follower status label from channel data.
329
- // Suppressed entirely when showFollowerStatus is false (e.g. the Linktree
330
- // official channel hides subscription status).
331
- const followerStatusLabel = React.useMemo(() => {
332
- if (!showFollowerStatus) return undefined
333
-
334
- const channelExtraData = (channel.data ?? {}) as {
335
- followerStatus?: string
336
- isFollower?: boolean
337
- }
338
-
339
- // If explicit followerStatus is provided, use it
340
- if (channelExtraData.followerStatus) {
341
- return String(channelExtraData.followerStatus)
342
- }
343
- // If isFollower is explicitly defined, use it to determine status
344
- if (channelExtraData.isFollower !== undefined) {
345
- return channelExtraData.isFollower
346
- ? 'Subscribed to you'
347
- : 'Not subscribed'
348
- }
349
- // Otherwise, don't show any status
350
- return undefined
351
- }, [channel.data, showFollowerStatus])
352
-
353
- const handleShowInfo = useCallback(() => {
354
- infoDialogRef.current?.showModal()
355
- }, [])
356
-
357
- const handleCloseInfo = useCallback(() => {
358
- infoDialogRef.current?.close()
359
- }, [])
360
-
361
339
  // Prevents all message instances from unmounting when ChannelViewInner re-renders
362
340
  const MessageOverride = useCallback(
363
341
  (props: MessageUIComponentProps) => {
@@ -394,10 +372,18 @@ const ChannelViewInner: React.FC<{
394
372
  <CustomChannelHeader
395
373
  onBack={onBack}
396
374
  showBackButton={showBackButton}
397
- onShowInfo={handleShowInfo}
398
- canShowInfo={Boolean(participant)}
375
+ showActionsMenu={showChannelInfo}
399
376
  showStarButton={showStarButton}
400
377
  dmAgentEnabled={showDmAgentHeader}
378
+ onLeaveConversation={onLeaveConversation}
379
+ onBlockParticipant={onBlockParticipant}
380
+ showDeleteConversation={showDeleteConversation}
381
+ showBlockParticipant={showBlockParticipant}
382
+ showReportParticipant={showReportParticipant}
383
+ onDeleteConversationClick={onDeleteConversationClick}
384
+ onBlockParticipantClick={onBlockParticipantClick}
385
+ onReportParticipantClick={onReportParticipantClick}
386
+ customChannelActions={customChannelActions}
401
387
  />
402
388
  </div>
403
389
 
@@ -434,28 +420,6 @@ const ChannelViewInner: React.FC<{
434
420
  />
435
421
  </Window>
436
422
  </WithComponents>
437
-
438
- {/* Channel Info Dialog */}
439
- <ChannelInfoDialog
440
- dialogRef={infoDialogRef}
441
- onClose={handleCloseInfo}
442
- participant={participant}
443
- participantDisplayName={resolveParticipantDisplayName(
444
- participant?.user
445
- )}
446
- channel={channel}
447
- followerStatusLabel={followerStatusLabel}
448
- onLeaveConversation={onLeaveConversation}
449
- onBlockParticipant={onBlockParticipant}
450
- showDeleteConversation={showDeleteConversation}
451
- showBlockParticipant={showBlockParticipant}
452
- showReportParticipant={showReportParticipant}
453
- onDeleteConversationClick={onDeleteConversationClick}
454
- onBlockParticipantClick={onBlockParticipantClick}
455
- onReportParticipantClick={onReportParticipantClick}
456
- customProfileContent={customProfileContent}
457
- customChannelActions={customChannelActions}
458
- />
459
423
  </>
460
424
  )
461
425
  }
@@ -481,7 +445,6 @@ export const ChannelView = React.memo<ChannelViewProps>(
481
445
  onReportParticipantClick,
482
446
  showBlockParticipant = true,
483
447
  showReportParticipant = true,
484
- showFollowerStatus = true,
485
448
  composerDisabled = false,
486
449
  composerDisabledReason,
487
450
  dmAgentEnabled,
@@ -490,13 +453,13 @@ export const ChannelView = React.memo<ChannelViewProps>(
490
453
  showStarButton = false,
491
454
  chatbotVotingEnabled = false,
492
455
  renderChannelBanner,
493
- customProfileContent,
494
456
  customChannelActions,
495
457
  renderMessage,
496
458
  onMessageLinkClick,
497
459
  sendButton,
498
460
  attachmentPreviewList,
499
461
  viewerLanguage,
462
+ showChannelInfo = true,
500
463
  }) => {
501
464
  // Custom send message handler that:
502
465
  // 1. Applies messageMetadata if provided
@@ -600,17 +563,16 @@ export const ChannelView = React.memo<ChannelViewProps>(
600
563
  onReportParticipantClick={onReportParticipantClick}
601
564
  showBlockParticipant={showBlockParticipant}
602
565
  showReportParticipant={showReportParticipant}
603
- showFollowerStatus={showFollowerStatus}
604
566
  composerDisabled={composerDisabled}
605
567
  composerDisabledReason={composerDisabledReason}
606
568
  showStarButton={showStarButton}
607
569
  dmAgentEnabled={dmAgentEnabled}
608
570
  chatbotVotingEnabled={chatbotVotingEnabled}
609
571
  renderChannelBanner={renderChannelBanner}
610
- customProfileContent={customProfileContent}
611
572
  customChannelActions={customChannelActions}
612
573
  renderMessage={renderMessage}
613
574
  viewerLanguage={viewerLanguage}
575
+ showChannelInfo={showChannelInfo}
614
576
  />
615
577
  </Channel>
616
578
  </DmAgentEnabledContext.Provider>
@@ -35,6 +35,7 @@ export const MessagingShell: React.FC<MessagingShellProps> = ({
35
35
  customChannelActions,
36
36
  renderMessage,
37
37
  onMessageLinkClick,
38
+ showChannelInfo,
38
39
  }) => {
39
40
  const {
40
41
  client,
@@ -256,6 +257,7 @@ export const MessagingShell: React.FC<MessagingShellProps> = ({
256
257
  customChannelActions={customChannelActions}
257
258
  renderMessage={renderMessage}
258
259
  onMessageLinkClick={onMessageLinkClick}
260
+ showChannelInfo={showChannelInfo}
259
261
  />
260
262
  </div>
261
263
  </div>
@@ -0,0 +1,227 @@
1
+ import { useEffect, useState } from 'react'
2
+ import type { Channel as ChannelType, ChannelMemberResponse } from 'stream-chat'
3
+
4
+ import { useMessagingContext } from '../providers/MessagingProvider'
5
+
6
+ // Blocked user from Stream Chat API
7
+ type BlockedUser = {
8
+ blocked_user_id: string
9
+ }
10
+
11
+ const REPORT_URL = 'https://linktr.ee/s/about/trust-center/report'
12
+
13
+ export interface UseChannelModerationActionsParams {
14
+ channel: ChannelType
15
+ participant: ChannelMemberResponse | undefined
16
+ /**
17
+ * When false, the blocked-status lookup is skipped (the result would be
18
+ * unused, e.g. the Linktree official channel does not offer blocking).
19
+ * Defaults to true.
20
+ */
21
+ showBlockParticipant?: boolean
22
+ /**
23
+ * When false, the blocked-status lookup is deferred. Useful for surfaces
24
+ * that mount the actions ahead of time (e.g. a closed popover) and only
25
+ * need block state once visible. Defaults to true.
26
+ */
27
+ enabled?: boolean
28
+ onLeaveConversation?: (channel: ChannelType) => void
29
+ onBlockParticipant?: (participantId?: string) => void
30
+ onDeleteConversationClick?: () => void
31
+ onBlockParticipantClick?: () => void
32
+ onReportParticipantClick?: () => void
33
+ /**
34
+ * Called after an action completes successfully (e.g. to close the
35
+ * surrounding dialog or popover).
36
+ */
37
+ onActionComplete?: () => void
38
+ /** Prefix used for debug logging. */
39
+ logLabel?: string
40
+ }
41
+
42
+ export interface ChannelModerationActions {
43
+ isParticipantBlocked: boolean
44
+ /**
45
+ * True while the initial blocked-status lookup is in flight. Until this is
46
+ * false, `isParticipantBlocked` has not yet been resolved and the
47
+ * block/unblock action should not be acted on.
48
+ */
49
+ isCheckingBlockedStatus: boolean
50
+ isLeaving: boolean
51
+ isUpdatingBlockStatus: boolean
52
+ handleLeaveConversation: () => Promise<void>
53
+ handleBlockUser: () => Promise<void>
54
+ handleUnblockUser: () => Promise<void>
55
+ handleReportUser: () => void
56
+ }
57
+
58
+ /**
59
+ * Encapsulates the conversation moderation actions (leave/delete, block,
60
+ * unblock, report) shared by the channel info dialog and the channel actions
61
+ * popover menu. Keeping the logic in one place ensures both surfaces stay in
62
+ * sync.
63
+ */
64
+ export const useChannelModerationActions = ({
65
+ channel,
66
+ participant,
67
+ showBlockParticipant = true,
68
+ enabled = true,
69
+ onLeaveConversation,
70
+ onBlockParticipant,
71
+ onDeleteConversationClick,
72
+ onBlockParticipantClick,
73
+ onReportParticipantClick,
74
+ onActionComplete,
75
+ logLabel = 'useChannelModerationActions',
76
+ }: UseChannelModerationActionsParams): ChannelModerationActions => {
77
+ const { service, debug } = useMessagingContext()
78
+ const [isParticipantBlocked, setIsParticipantBlocked] = useState(false)
79
+ const [isCheckingBlockedStatus, setIsCheckingBlockedStatus] =
80
+ useState(false)
81
+ const [isLeaving, setIsLeaving] = useState(false)
82
+ const [isUpdatingBlockStatus, setIsUpdatingBlockStatus] = useState(false)
83
+
84
+ // Resolve whether the participant is blocked whenever the participant or
85
+ // surface visibility changes.
86
+ useEffect(() => {
87
+ // When the lookup is skipped (Block action hidden, surface disabled, or no
88
+ // participant), clear any stale blocked state so a previous participant's
89
+ // value can't leak into the next conversation.
90
+ if (
91
+ !enabled ||
92
+ !showBlockParticipant ||
93
+ !service ||
94
+ !participant?.user?.id
95
+ ) {
96
+ setIsParticipantBlocked(false)
97
+ setIsCheckingBlockedStatus(false)
98
+ return
99
+ }
100
+
101
+ let cancelled = false
102
+ const participantId = participant.user.id
103
+
104
+ setIsCheckingBlockedStatus(true)
105
+
106
+ void (async () => {
107
+ try {
108
+ const blockedUsers = await service.getBlockedUsers()
109
+ if (cancelled) return
110
+ setIsParticipantBlocked(
111
+ blockedUsers.some(
112
+ (user: BlockedUser) => user.blocked_user_id === participantId
113
+ )
114
+ )
115
+ } catch (error) {
116
+ if (!cancelled) {
117
+ console.error(`[${logLabel}] Failed to check blocked status:`, error)
118
+ }
119
+ } finally {
120
+ if (!cancelled) setIsCheckingBlockedStatus(false)
121
+ }
122
+ })()
123
+
124
+ // Ignore an in-flight result if the participant/surface changes first.
125
+ return () => {
126
+ cancelled = true
127
+ }
128
+ }, [enabled, service, participant?.user?.id, showBlockParticipant, logLabel])
129
+
130
+ const handleLeaveConversation = async () => {
131
+ if (isLeaving) return
132
+
133
+ // Fire analytics callback before action
134
+ onDeleteConversationClick?.()
135
+
136
+ if (debug) {
137
+ console.log(`[${logLabel}] Leave conversation`, channel.cid)
138
+ }
139
+ setIsLeaving(true)
140
+
141
+ try {
142
+ const actingUserId = channel._client?.userID ?? null
143
+ await channel.hide(actingUserId, false)
144
+
145
+ if (onLeaveConversation) {
146
+ await onLeaveConversation(channel)
147
+ }
148
+
149
+ onActionComplete?.()
150
+ } catch (error) {
151
+ console.error(`[${logLabel}] Failed to leave conversation`, error)
152
+ } finally {
153
+ setIsLeaving(false)
154
+ }
155
+ }
156
+
157
+ const handleBlockUser = async () => {
158
+ if (isUpdatingBlockStatus || !service) return
159
+
160
+ // Fire analytics callback before action
161
+ onBlockParticipantClick?.()
162
+
163
+ if (debug) {
164
+ console.log(`[${logLabel}] Block member`, participant?.user?.id)
165
+ }
166
+ setIsUpdatingBlockStatus(true)
167
+
168
+ try {
169
+ await service.blockUser(participant?.user?.id)
170
+
171
+ if (onBlockParticipant) {
172
+ await onBlockParticipant(participant?.user?.id)
173
+ }
174
+
175
+ onActionComplete?.()
176
+ } catch (error) {
177
+ console.error(`[${logLabel}] Failed to block member`, error)
178
+ } finally {
179
+ setIsUpdatingBlockStatus(false)
180
+ }
181
+ }
182
+
183
+ const handleUnblockUser = async () => {
184
+ if (isUpdatingBlockStatus || !service) return
185
+
186
+ // Fire analytics callback before action
187
+ onBlockParticipantClick?.()
188
+
189
+ if (debug) {
190
+ console.log(`[${logLabel}] Unblock member`, participant?.user?.id)
191
+ }
192
+ setIsUpdatingBlockStatus(true)
193
+
194
+ try {
195
+ await service.unBlockUser(participant?.user?.id)
196
+
197
+ if (onBlockParticipant) {
198
+ await onBlockParticipant(participant?.user?.id)
199
+ }
200
+
201
+ onActionComplete?.()
202
+ } catch (error) {
203
+ console.error(`[${logLabel}] Failed to unblock member`, error)
204
+ } finally {
205
+ setIsUpdatingBlockStatus(false)
206
+ }
207
+ }
208
+
209
+ const handleReportUser = () => {
210
+ // Fire analytics callback before action
211
+ onReportParticipantClick?.()
212
+
213
+ onActionComplete?.()
214
+ window.open(REPORT_URL, '_blank', 'noopener,noreferrer')
215
+ }
216
+
217
+ return {
218
+ isParticipantBlocked,
219
+ isCheckingBlockedStatus,
220
+ isLeaving,
221
+ isUpdatingBlockStatus,
222
+ handleLeaveConversation,
223
+ handleBlockUser,
224
+ handleUnblockUser,
225
+ handleReportUser,
226
+ }
227
+ }
package/src/index.ts CHANGED
@@ -48,7 +48,6 @@ export type { ParticipantDisplayUser } from './utils/resolveParticipantDisplayNa
48
48
 
49
49
  // Types
50
50
  export type {
51
- ChannelListDerivedState,
52
51
  MessagingShellProps,
53
52
  ChannelListProps,
54
53
  ChannelViewProps,
package/src/types.ts CHANGED
@@ -52,36 +52,6 @@ export interface MessagingCapabilities {
52
52
  showDeleteConversation?: boolean
53
53
  }
54
54
 
55
- /**
56
- * Derived state reported by the mounted ChannelList instance.
57
- *
58
- * This reflects the currently loaded list data only. It does not guarantee
59
- * complete server-side truth for channels outside the mounted list's current
60
- * query window or cache.
61
- */
62
- export interface ChannelListDerivedState {
63
- /**
64
- * Whether the mounted list has completed its first channel-resolution pass.
65
- */
66
- isInitialLoadSettled: boolean
67
- /**
68
- * Visible channels after `channelRenderFilterFn` is applied.
69
- */
70
- channels: Channel[]
71
- /**
72
- * Raw channels received from Stream before client-side filtering.
73
- */
74
- rawChannels: Channel[]
75
- /**
76
- * Convenience flag for empty-state consumers. Mirrors `channels.length > 0`.
77
- */
78
- hasChannels: boolean
79
- /**
80
- * Number of visible channels with at least one unread message.
81
- */
82
- unreadCount: number
83
- }
84
-
85
55
  /**
86
56
  * ChannelList component props
87
57
  */
@@ -127,14 +97,6 @@ export interface ChannelListProps {
127
97
  * Falls back to message.text when no matching translation exists.
128
98
  */
129
99
  viewerLanguage?: string
130
- /**
131
- * Reports derived state from the mounted Stream ChannelList instance.
132
- *
133
- * This callback is driven by currently loaded list data only. Use it for
134
- * cache/list-derived UI state such as empty-state confirmation or visible
135
- * unread badges; do not assume it represents complete server-side truth.
136
- */
137
- onStateChange?: (state: ChannelListDerivedState) => void
138
100
  }
139
101
 
140
102
  /**
@@ -184,9 +146,11 @@ export interface ChannelViewProps {
184
146
  showReportParticipant?: boolean
185
147
 
186
148
  /**
187
- * Show the subscription/follower-status label in the channel info dialog
188
- * profile card. Defaults to true. Set false for restricted surfaces such
189
- * as the Linktree official channel, where subscription status is hidden.
149
+ * @deprecated Not currently rendered. The channel info sidebar was removed
150
+ * in favour of the actions popover; this prop is retained for API
151
+ * compatibility and will be wired up again when the replacement profile
152
+ * surface lands. Previously toggled the subscription/follower-status label
153
+ * in the channel info dialog profile card.
190
154
  */
191
155
  showFollowerStatus?: boolean
192
156
 
@@ -245,6 +209,19 @@ export interface ChannelViewProps {
245
209
  */
246
210
  showStarButton?: boolean
247
211
 
212
+ /**
213
+ * Show the channel actions menu (the `...` popover) in the header, which
214
+ * exposes the block/report/delete moderation actions. Defaults to true.
215
+ * Set false for surfaces that should not expose those actions — e.g.
216
+ * anonymous visitor chat, where the visitor has no authenticated identity
217
+ * to act on.
218
+ *
219
+ * Note: the channel info sidebar was removed in favour of the actions
220
+ * popover, so this no longer mounts a profile dialog or renders a clickable
221
+ * participant name; it now solely gates the actions menu.
222
+ */
223
+ showChannelInfo?: boolean
224
+
248
225
  /**
249
226
  * Enable thumbs up/down voting on chatbot messages.
250
227
  * When true, vote buttons render below chatbot (DM Agent) messages.
@@ -266,17 +243,16 @@ export interface ChannelViewProps {
266
243
  renderChannelBanner?: () => React.ReactNode
267
244
 
268
245
  /**
269
- * Custom content rendered below the participant name and contact details
270
- * in the channel info dialog profile card.
271
- * Useful for badges (e.g. follower status), metadata, or any extra info.
272
- *
273
- * @example
274
- * customProfileContent={<SubscriptionBadge isFollower={channel.data?.isFollower} />}
246
+ * @deprecated Not currently rendered. The channel info sidebar was removed
247
+ * in favour of the actions popover; this prop is retained for API
248
+ * compatibility and will be wired up again when the replacement profile
249
+ * surface lands. Previously rendered custom content (badges, metadata)
250
+ * below the participant name in the channel info dialog profile card.
275
251
  */
276
252
  customProfileContent?: React.ReactNode
277
253
 
278
254
  /**
279
- * Custom actions rendered at the bottom of the channel info dialog
255
+ * Custom actions rendered at the bottom of the channel actions popover
280
256
  * (below Delete Conversation, Block/Unblock, Report).
281
257
  * Pass one or more <li> elements so they match the list styling.
282
258
  * Use the exported ActionButton for consistent styling.
@@ -347,6 +323,7 @@ export type ChannelViewPassthroughProps = Pick<
347
323
  | 'customChannelActions'
348
324
  | 'renderMessage'
349
325
  | 'onMessageLinkClick'
326
+ | 'showChannelInfo'
350
327
  >
351
328
 
352
329
  /**