@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.
- package/README.md +1 -18
- package/dist/{Card-jyXjZZ0u.js → Card-BAc2cgtn.js} +3 -3
- package/dist/{Card-jyXjZZ0u.js.map → Card-BAc2cgtn.js.map} +1 -1
- package/dist/{Card-D_XOj1eE.cjs → Card-Cn1cBVnr.cjs} +2 -2
- package/dist/{Card-D_XOj1eE.cjs.map → Card-Cn1cBVnr.cjs.map} +1 -1
- package/dist/{Card-BkgsPkp4.cjs → Card-DAyszUxa.cjs} +2 -2
- package/dist/{Card-BkgsPkp4.cjs.map → Card-DAyszUxa.cjs.map} +1 -1
- package/dist/{Card-BwFdJXYm.js → Card-D_2VQScd.js} +2 -2
- package/dist/{Card-BwFdJXYm.js.map → Card-D_2VQScd.js.map} +1 -1
- package/dist/{Card-B9atg4sP.js → Card-D_G8133I.js} +2 -2
- package/dist/{Card-B9atg4sP.js.map → Card-D_G8133I.js.map} +1 -1
- package/dist/{Card-1U2tLPcp.cjs → Card-gYxPXe_W.cjs} +2 -2
- package/dist/{Card-1U2tLPcp.cjs.map → Card-gYxPXe_W.cjs.map} +1 -1
- package/dist/{LockedThumbnail-Dwt_goCX.js → LockedThumbnail-C7tWpOQr.js} +2 -2
- package/dist/{LockedThumbnail-Dwt_goCX.js.map → LockedThumbnail-C7tWpOQr.js.map} +1 -1
- package/dist/{LockedThumbnail-oxtdpgut.cjs → LockedThumbnail-DtOTZl3l.cjs} +2 -2
- package/dist/{LockedThumbnail-oxtdpgut.cjs.map → LockedThumbnail-DtOTZl3l.cjs.map} +1 -1
- package/dist/{index-CO975B6P.js → index-C_NFzAB9.js} +1228 -1289
- package/dist/index-C_NFzAB9.js.map +1 -0
- package/dist/index-_Se6ovQm.cjs +2 -0
- package/dist/index-_Se6ovQm.cjs.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +24 -49
- package/dist/index.js +1 -1
- package/package.json +4 -3
- package/src/components/ChannelActionsMenu/ChannelActionsMenu.test.tsx +305 -0
- package/src/components/ChannelActionsMenu/index.tsx +221 -0
- package/src/components/ChannelList/index.test.tsx +3 -151
- package/src/components/ChannelList/index.tsx +4 -72
- package/src/components/ChannelView.stories.tsx +3 -73
- package/src/components/ChannelView.test.tsx +33 -29
- package/src/components/ChannelView.tsx +71 -109
- package/src/components/MessagingShell/index.tsx +2 -0
- package/src/hooks/useChannelModerationActions.ts +227 -0
- package/src/index.ts +0 -1
- package/src/types.ts +25 -48
- package/dist/index-CO975B6P.js.map +0 -1
- package/dist/index-D4Dse1Lu.cjs +0 -2
- package/dist/index-D4Dse1Lu.cjs.map +0 -1
- package/src/components/ChannelInfoDialog/ChannelInfoDialog.test.tsx +0 -333
- 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 {
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
{
|
|
223
|
-
<
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
|
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
|
-
|
|
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
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
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
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
|
-
*
|
|
270
|
-
* in the
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
*
|
|
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
|
|
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
|
/**
|