@linktr.ee/messaging-react 3.2.0 → 3.3.1

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 (37) hide show
  1. package/dist/{Card-bdnjL_4d.js → Card-BAc2cgtn.js} +3 -3
  2. package/dist/{Card-bdnjL_4d.js.map → Card-BAc2cgtn.js.map} +1 -1
  3. package/dist/{Card-CO089n1e.cjs → Card-Cn1cBVnr.cjs} +2 -2
  4. package/dist/{Card-CO089n1e.cjs.map → Card-Cn1cBVnr.cjs.map} +1 -1
  5. package/dist/{Card-B5TCecD6.cjs → Card-DAyszUxa.cjs} +2 -2
  6. package/dist/{Card-B5TCecD6.cjs.map → Card-DAyszUxa.cjs.map} +1 -1
  7. package/dist/{Card-DQYLHbDI.js → Card-D_2VQScd.js} +2 -2
  8. package/dist/{Card-DQYLHbDI.js.map → Card-D_2VQScd.js.map} +1 -1
  9. package/dist/{Card-DTaHgygz.js → Card-D_G8133I.js} +2 -2
  10. package/dist/{Card-DTaHgygz.js.map → Card-D_G8133I.js.map} +1 -1
  11. package/dist/{Card-aO1qZWDU.cjs → Card-gYxPXe_W.cjs} +2 -2
  12. package/dist/{Card-aO1qZWDU.cjs.map → Card-gYxPXe_W.cjs.map} +1 -1
  13. package/dist/{LockedThumbnail-nsFA3DjA.js → LockedThumbnail-C7tWpOQr.js} +2 -2
  14. package/dist/{LockedThumbnail-nsFA3DjA.js.map → LockedThumbnail-C7tWpOQr.js.map} +1 -1
  15. package/dist/{LockedThumbnail-CWVybsBb.cjs → LockedThumbnail-DtOTZl3l.cjs} +2 -2
  16. package/dist/{LockedThumbnail-CWVybsBb.cjs.map → LockedThumbnail-DtOTZl3l.cjs.map} +1 -1
  17. package/dist/{index-DJKFVBkP.js → index-C_NFzAB9.js} +1356 -1382
  18. package/dist/index-C_NFzAB9.js.map +1 -0
  19. package/dist/index-_Se6ovQm.cjs +2 -0
  20. package/dist/index-_Se6ovQm.cjs.map +1 -0
  21. package/dist/index.cjs +1 -1
  22. package/dist/index.d.ts +39 -15
  23. package/dist/index.js +1 -1
  24. package/package.json +3 -3
  25. package/src/components/ChannelActionsMenu/ChannelActionsMenu.test.tsx +305 -0
  26. package/src/components/ChannelActionsMenu/index.tsx +221 -0
  27. package/src/components/ChannelView.stories.tsx +3 -73
  28. package/src/components/ChannelView.test.tsx +30 -57
  29. package/src/components/ChannelView.tsx +66 -115
  30. package/src/hooks/useChannelModerationActions.ts +227 -0
  31. package/src/stream-custom-data.ts +21 -1
  32. package/src/types.ts +20 -15
  33. package/dist/index-BO2VfA-M.cjs +0 -2
  34. package/dist/index-BO2VfA-M.cjs.map +0 -1
  35. package/dist/index-DJKFVBkP.js.map +0 -1
  36. package/src/components/ChannelInfoDialog/ChannelInfoDialog.test.tsx +0 -333
  37. package/src/components/ChannelInfoDialog/index.tsx +0 -336
@@ -1,333 +0,0 @@
1
- import React from 'react'
2
- import type { Channel, ChannelMemberResponse } from 'stream-chat'
3
- import { describe, expect, it, vi, beforeEach } from 'vitest'
4
-
5
- import { renderWithProviders, screen, userEvent, waitFor } from '../../test/utils'
6
-
7
- import { ChannelInfoDialog } from './index'
8
-
9
- const { getBlockedUsersMock } = vi.hoisted(() => ({
10
- getBlockedUsersMock: vi.fn().mockResolvedValue([]),
11
- }))
12
-
13
- vi.mock('../../providers/MessagingProvider', () => ({
14
- useMessagingContext: () => ({
15
- service: {
16
- getBlockedUsers: getBlockedUsersMock,
17
- blockUser: vi.fn().mockResolvedValue(undefined),
18
- unBlockUser: vi.fn().mockResolvedValue(undefined),
19
- },
20
- debug: false,
21
- }),
22
- }))
23
-
24
- vi.mock('../Avatar', () => ({
25
- Avatar: ({ name }: { name: string }) => (
26
- <div data-testid="avatar">{name}</div>
27
- ),
28
- }))
29
-
30
- vi.mock('../CloseButton', () => ({
31
- CloseButton: ({ onClick }: { onClick: () => void }) => (
32
- <button data-testid="close-button" onClick={onClick} />
33
- ),
34
- }))
35
-
36
- vi.mock('../ActionButton', () => ({
37
- default: ({
38
- children,
39
- onClick,
40
- }: {
41
- children: React.ReactNode
42
- onClick?: () => void
43
- }) => (
44
- <button data-testid="action-button" onClick={onClick}>
45
- {children}
46
- </button>
47
- ),
48
- }))
49
-
50
- vi.mock('@phosphor-icons/react', () => ({
51
- FlagIcon: () => <span data-testid="flag-icon" />,
52
- ProhibitInsetIcon: () => <span data-testid="prohibit-icon" />,
53
- SignOutIcon: () => <span data-testid="signout-icon" />,
54
- SpinnerGapIcon: () => <span data-testid="spinner-icon" />,
55
- }))
56
-
57
- const createChannel = () =>
58
- ({
59
- id: 'channel-1',
60
- cid: 'messaging:channel-1',
61
- data: {},
62
- _client: { userID: 'visitor-1' },
63
- state: {
64
- members: {},
65
- membership: {},
66
- messages: [],
67
- },
68
- hide: vi.fn(),
69
- }) as unknown as Channel
70
-
71
- const createParticipant = (
72
- overrides: Partial<{ name: string; username: string }> = {}
73
- ) =>
74
- ({
75
- user: {
76
- id: 'linker-1',
77
- name: overrides.name ?? 'Linker',
78
- image: undefined,
79
- username: overrides.username,
80
- },
81
- role: 'member',
82
- }) as unknown as ChannelMemberResponse
83
-
84
- const defaultProps = () => ({
85
- dialogRef: { current: document.createElement('dialog') },
86
- onClose: vi.fn(),
87
- channel: createChannel(),
88
- participant: createParticipant(),
89
- })
90
-
91
- describe('ChannelInfoDialog', () => {
92
- beforeEach(() => {
93
- vi.clearAllMocks()
94
- })
95
-
96
- it('renders participant name', () => {
97
- renderWithProviders(
98
- <ChannelInfoDialog {...defaultProps()} />
99
- )
100
-
101
- const nameEl = screen.getByText('Linker', { selector: 'p' })
102
- expect(nameEl).toBeInTheDocument()
103
- })
104
-
105
- it('uses participantDisplayName when provided', () => {
106
- renderWithProviders(
107
- <ChannelInfoDialog
108
- {...defaultProps()}
109
- participantDisplayName="Custom Label"
110
- />
111
- )
112
-
113
- expect(screen.getByText('Custom Label', { selector: 'p' })).toBeInTheDocument()
114
- })
115
-
116
- it('falls back to username when name is UUID-like', () => {
117
- renderWithProviders(
118
- <ChannelInfoDialog
119
- {...defaultProps()}
120
- participant={createParticipant({
121
- name: 'a1b2c3d4-e5f6-4789-a012-3456789abcde',
122
- username: 'linkeruser',
123
- })}
124
- />
125
- )
126
-
127
- expect(screen.getByText('linkeruser', { selector: 'p' })).toBeInTheDocument()
128
- })
129
-
130
- it('shows Unknown member when name matches user id', () => {
131
- renderWithProviders(
132
- <ChannelInfoDialog
133
- {...defaultProps()}
134
- participant={{
135
- user: {
136
- id: 'opaque-user-id',
137
- name: 'opaque-user-id',
138
- },
139
- role: 'member',
140
- } as unknown as ChannelMemberResponse}
141
- />
142
- )
143
-
144
- expect(
145
- screen.getByText('Unknown member', { selector: 'p' })
146
- ).toBeInTheDocument()
147
- })
148
-
149
- it('renders participant username as secondary info', () => {
150
- renderWithProviders(
151
- <ChannelInfoDialog
152
- {...defaultProps()}
153
- participant={createParticipant({ username: 'linkeruser' })}
154
- />
155
- )
156
-
157
- expect(screen.getByText('linktr.ee/linkeruser')).toBeInTheDocument()
158
- })
159
-
160
- it('renders customProfileContent when provided', () => {
161
- renderWithProviders(
162
- <ChannelInfoDialog
163
- {...defaultProps()}
164
- customProfileContent={
165
- <span data-testid="custom-profile">Subscriber Badge</span>
166
- }
167
- />
168
- )
169
-
170
- expect(screen.getByTestId('custom-profile')).toHaveTextContent(
171
- 'Subscriber Badge'
172
- )
173
- })
174
-
175
- it('renders customChannelActions when provided', () => {
176
- renderWithProviders(
177
- <ChannelInfoDialog
178
- {...defaultProps()}
179
- customChannelActions={
180
- <li data-testid="custom-action">Custom Action</li>
181
- }
182
- />
183
- )
184
-
185
- expect(screen.getByTestId('custom-action')).toHaveTextContent(
186
- 'Custom Action'
187
- )
188
- })
189
-
190
- it('renders followerStatusLabel when provided', () => {
191
- renderWithProviders(
192
- <ChannelInfoDialog
193
- {...defaultProps()}
194
- followerStatusLabel="Subscribed to you"
195
- />
196
- )
197
-
198
- expect(screen.getByText('Subscribed to you')).toBeInTheDocument()
199
- })
200
-
201
- it('hides followerStatusLabel when customProfileContent is provided', () => {
202
- renderWithProviders(
203
- <ChannelInfoDialog
204
- {...defaultProps()}
205
- followerStatusLabel="Subscribed to you"
206
- customProfileContent={
207
- <span data-testid="custom-profile">Custom Badge</span>
208
- }
209
- />
210
- )
211
-
212
- expect(
213
- screen.queryByText('Subscribed to you')
214
- ).not.toBeInTheDocument()
215
- expect(screen.getByTestId('custom-profile')).toHaveTextContent(
216
- 'Custom Badge'
217
- )
218
- })
219
-
220
- it('shows Delete Conversation button when showDeleteConversation is true', () => {
221
- renderWithProviders(
222
- <ChannelInfoDialog {...defaultProps()} showDeleteConversation={true} />
223
- )
224
-
225
- expect(screen.getByText('Delete Conversation')).toBeInTheDocument()
226
- })
227
-
228
- it('hides Delete Conversation button when showDeleteConversation is false', () => {
229
- renderWithProviders(
230
- <ChannelInfoDialog {...defaultProps()} showDeleteConversation={false} />
231
- )
232
-
233
- expect(screen.queryByText('Delete Conversation')).not.toBeInTheDocument()
234
- })
235
-
236
- it('calls onDeleteConversationClick when Delete Conversation is clicked', async () => {
237
- const onDeleteConversationClick = vi.fn()
238
- const props = defaultProps()
239
- renderWithProviders(
240
- <ChannelInfoDialog
241
- {...props}
242
- onDeleteConversationClick={onDeleteConversationClick}
243
- />
244
- )
245
-
246
- await userEvent.click(screen.getByText('Delete Conversation').closest('button')!)
247
- await waitFor(() => {
248
- expect(onDeleteConversationClick).toHaveBeenCalledOnce()
249
- })
250
- })
251
-
252
- it('calls onBlockParticipantClick when Block is clicked', async () => {
253
- const onBlockParticipantClick = vi.fn()
254
- renderWithProviders(
255
- <ChannelInfoDialog
256
- {...defaultProps()}
257
- onBlockParticipantClick={onBlockParticipantClick}
258
- />
259
- )
260
-
261
- await userEvent.click(screen.getByText('Block').closest('button')!)
262
- await waitFor(() => {
263
- expect(onBlockParticipantClick).toHaveBeenCalledOnce()
264
- })
265
- })
266
-
267
- it('calls onReportParticipantClick when Report is clicked', async () => {
268
- const onReportParticipantClick = vi.fn()
269
- const windowOpenSpy = vi.spyOn(window, 'open').mockImplementation(() => null)
270
-
271
- renderWithProviders(
272
- <ChannelInfoDialog
273
- {...defaultProps()}
274
- onReportParticipantClick={onReportParticipantClick}
275
- />
276
- )
277
-
278
- await userEvent.click(screen.getByText('Report').closest('button')!)
279
- await waitFor(() => {
280
- expect(onReportParticipantClick).toHaveBeenCalledOnce()
281
- })
282
- windowOpenSpy.mockRestore()
283
- })
284
-
285
- it('shows Block and Report actions by default', () => {
286
- renderWithProviders(<ChannelInfoDialog {...defaultProps()} />)
287
-
288
- expect(screen.getByText('Block')).toBeInTheDocument()
289
- expect(screen.getByText('Report')).toBeInTheDocument()
290
- })
291
-
292
- it('hides the Block action when showBlockParticipant is false', () => {
293
- renderWithProviders(
294
- <ChannelInfoDialog {...defaultProps()} showBlockParticipant={false} />
295
- )
296
-
297
- expect(screen.queryByText('Block')).not.toBeInTheDocument()
298
- expect(screen.queryByText('Unblock')).not.toBeInTheDocument()
299
- })
300
-
301
- it('fetches blocked users by default (Block action shown)', () => {
302
- renderWithProviders(<ChannelInfoDialog {...defaultProps()} />)
303
-
304
- expect(getBlockedUsersMock).toHaveBeenCalled()
305
- })
306
-
307
- it('does not fetch blocked users when showBlockParticipant is false', () => {
308
- renderWithProviders(
309
- <ChannelInfoDialog {...defaultProps()} showBlockParticipant={false} />
310
- )
311
-
312
- expect(getBlockedUsersMock).not.toHaveBeenCalled()
313
- })
314
-
315
- it('hides the Report action when showReportParticipant is false', () => {
316
- renderWithProviders(
317
- <ChannelInfoDialog {...defaultProps()} showReportParticipant={false} />
318
- )
319
-
320
- expect(screen.queryByText('Report')).not.toBeInTheDocument()
321
- })
322
-
323
- it('returns null when participant is undefined', () => {
324
- const { container } = renderWithProviders(
325
- <ChannelInfoDialog
326
- {...defaultProps()}
327
- participant={undefined}
328
- />
329
- )
330
-
331
- expect(container.querySelector('dialog')).not.toBeInTheDocument()
332
- })
333
- })
@@ -1,336 +0,0 @@
1
- import {
2
- FlagIcon,
3
- ProhibitInsetIcon,
4
- SignOutIcon,
5
- SpinnerGapIcon,
6
- } from '@phosphor-icons/react'
7
- import React, { useState, useCallback, useEffect } from 'react'
8
- import type { Channel as ChannelType, ChannelMemberResponse } from 'stream-chat'
9
-
10
- import { useMessagingContext } from '../../providers/MessagingProvider'
11
- import { resolveParticipantDisplayName } from '../../utils/resolveParticipantDisplayName'
12
- import ActionButton from '../ActionButton'
13
- import { Avatar } from '../Avatar'
14
- import { CloseButton } from '../CloseButton'
15
-
16
- // Custom user type with username
17
- type CustomUser = {
18
- username?: string
19
- }
20
-
21
- // Blocked user from Stream Chat API
22
- type BlockedUser = {
23
- blocked_user_id: string
24
- }
25
-
26
- export interface ChannelInfoDialogProps {
27
- dialogRef: React.RefObject<HTMLDialogElement>
28
- onClose: () => void
29
- participant: ChannelMemberResponse | undefined
30
- participantDisplayName?: string
31
- channel: ChannelType
32
- followerStatusLabel?: string
33
- onLeaveConversation?: (channel: ChannelType) => void
34
- onBlockParticipant?: (participantId?: string) => void
35
- showDeleteConversation?: boolean
36
- /**
37
- * Show the Block/Unblock action. Defaults to true.
38
- * Set false to hide it (e.g. the Linktree official channel).
39
- */
40
- showBlockParticipant?: boolean
41
- /**
42
- * Show the Report action. Defaults to true.
43
- * Set false to hide it (e.g. the Linktree official channel).
44
- */
45
- showReportParticipant?: boolean
46
- onDeleteConversationClick?: () => void
47
- onBlockParticipantClick?: () => void
48
- onReportParticipantClick?: () => void
49
- customProfileContent?: React.ReactNode
50
- customChannelActions?: React.ReactNode
51
- }
52
-
53
- /**
54
- * Channel info dialog (matching original implementation)
55
- */
56
- export const ChannelInfoDialog: React.FC<ChannelInfoDialogProps> = ({
57
- dialogRef,
58
- onClose,
59
- participant,
60
- participantDisplayName,
61
- channel,
62
- followerStatusLabel,
63
- onLeaveConversation,
64
- onBlockParticipant,
65
- showDeleteConversation = true,
66
- showBlockParticipant = true,
67
- showReportParticipant = true,
68
- onDeleteConversationClick,
69
- onBlockParticipantClick,
70
- onReportParticipantClick,
71
- customProfileContent,
72
- customChannelActions,
73
- }) => {
74
- const { service, debug } = useMessagingContext()
75
- const [isParticipantBlocked, setIsParticipantBlocked] = useState(false)
76
- const [isLeaving, setIsLeaving] = useState(false)
77
- const [isUpdatingBlockStatus, setIsUpdatingBlockStatus] = useState(false)
78
-
79
- // Check if participant is blocked when participant changes.
80
- // Skipped when the Block action is hidden — the result would be unused
81
- // (e.g. the Linktree official channel does not offer blocking).
82
- const checkIsParticipantBlocked = useCallback(async () => {
83
- if (!showBlockParticipant || !service || !participant?.user?.id) return
84
-
85
- try {
86
- const blockedUsers = await service.getBlockedUsers()
87
- const isBlocked = blockedUsers.some(
88
- (user: BlockedUser) => user.blocked_user_id === participant?.user?.id
89
- )
90
- setIsParticipantBlocked(isBlocked)
91
- } catch (error) {
92
- console.error(
93
- '[ChannelInfoDialog] Failed to check blocked status:',
94
- error
95
- )
96
- }
97
- }, [service, participant?.user?.id, showBlockParticipant])
98
-
99
- useEffect(() => {
100
- checkIsParticipantBlocked()
101
- }, [checkIsParticipantBlocked])
102
-
103
- const handleLeaveConversation = async () => {
104
- if (isLeaving) return
105
-
106
- // Fire analytics callback before action
107
- onDeleteConversationClick?.()
108
-
109
- if (debug) {
110
- console.log('[ChannelInfoDialog] Leave conversation', channel.cid)
111
- }
112
- setIsLeaving(true)
113
-
114
- try {
115
- const actingUserId = channel._client?.userID ?? null
116
- await channel.hide(actingUserId, false)
117
-
118
- if (onLeaveConversation) {
119
- await onLeaveConversation(channel)
120
- }
121
-
122
- onClose()
123
- } catch (error) {
124
- console.error('[ChannelInfoDialog] Failed to leave conversation', error)
125
- } finally {
126
- setIsLeaving(false)
127
- }
128
- }
129
-
130
- const handleBlockUser = async () => {
131
- if (isUpdatingBlockStatus || !service) return
132
-
133
- // Fire analytics callback before action
134
- onBlockParticipantClick?.()
135
-
136
- if (debug) {
137
- console.log('[ChannelInfoDialog] Block member', participant?.user?.id)
138
- }
139
- setIsUpdatingBlockStatus(true)
140
-
141
- try {
142
- await service.blockUser(participant?.user?.id)
143
-
144
- if (onBlockParticipant) {
145
- await onBlockParticipant(participant?.user?.id)
146
- }
147
-
148
- onClose()
149
- } catch (error) {
150
- console.error('[ChannelInfoDialog] Failed to block member', error)
151
- } finally {
152
- setIsUpdatingBlockStatus(false)
153
- }
154
- }
155
-
156
- const handleUnblockUser = async () => {
157
- if (isUpdatingBlockStatus || !service) return
158
-
159
- // Fire analytics callback before action
160
- onBlockParticipantClick?.()
161
-
162
- if (debug) {
163
- console.log('[ChannelInfoDialog] Unblock member', participant?.user?.id)
164
- }
165
- setIsUpdatingBlockStatus(true)
166
-
167
- try {
168
- await service.unBlockUser(participant?.user?.id)
169
-
170
- if (onBlockParticipant) {
171
- await onBlockParticipant(participant?.user?.id)
172
- }
173
-
174
- onClose()
175
- } catch (error) {
176
- console.error('[ChannelInfoDialog] Failed to unblock member', error)
177
- } finally {
178
- setIsUpdatingBlockStatus(false)
179
- }
180
- }
181
-
182
- const handleReportUser = () => {
183
- // Fire analytics callback before action
184
- onReportParticipantClick?.()
185
-
186
- onClose()
187
- window.open(
188
- 'https://linktr.ee/s/about/trust-center/report',
189
- '_blank',
190
- 'noopener,noreferrer'
191
- )
192
- }
193
-
194
- if (!participant) return null
195
-
196
- const participantName =
197
- participantDisplayName ??
198
- resolveParticipantDisplayName(participant.user)
199
- const participantImage = participant.user?.image
200
- const participantUsername = (participant.user as CustomUser)?.username
201
- const participantSecondary = participantUsername
202
- ? `linktr.ee/${participantUsername}`
203
- : undefined
204
- const participantId = participant.user?.id || 'unknown'
205
-
206
- return (
207
- // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions
208
- <dialog
209
- ref={dialogRef}
210
- className="mes-dialog group"
211
- onClose={onClose}
212
- onClick={(e) => {
213
- if (e.target === dialogRef.current) {
214
- onClose()
215
- }
216
- }}
217
- >
218
- <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">
219
- <div className="flex items-center justify-between border-b border-sand px-4 py-3">
220
- <h2 className="text-base font-semibold text-charcoal">Chat info</h2>
221
- <CloseButton onClick={onClose} />
222
- </div>
223
-
224
- <div className="flex-1 px-2 overflow-y-auto w-full">
225
- <div
226
- className="flex flex-col items-center gap-3 self-stretch px-4 py-2 mt-6 rounded-lg border border-black/[0.04]"
227
- style={{ backgroundColor: '#FBFAF9' }}
228
- >
229
- <div className="flex items-center gap-3 w-full">
230
- <Avatar
231
- id={participantId}
232
- name={participantName}
233
- image={participantImage}
234
- size={88}
235
- shape="circle"
236
- />
237
- <div className="flex flex-col min-w-0 flex-1">
238
- <p className="truncate text-base font-semibold text-charcoal">
239
- {participantName}
240
- </p>
241
- {participantSecondary && (
242
- <p className="truncate text-sm text-[#00000055]">
243
- {participantSecondary}
244
- </p>
245
- )}
246
- {/* Deprecated: use customProfileContent instead. followerStatusLabel will be removed in a future release. */}
247
- {followerStatusLabel && !customProfileContent && (
248
- <span
249
- className="mt-1 rounded-full text-xs font-normal w-fit"
250
- style={{
251
- padding: '4px 8px',
252
- backgroundColor:
253
- followerStatusLabel === 'Subscribed to you'
254
- ? '#DCFCE7'
255
- : '#F5F5F4',
256
- color:
257
- followerStatusLabel === 'Subscribed to you'
258
- ? '#008236'
259
- : '#78716C',
260
- lineHeight: '133.333%',
261
- letterSpacing: '0.21px',
262
- }}
263
- >
264
- {followerStatusLabel}
265
- </span>
266
- )}
267
- </div>
268
- </div>
269
- {customProfileContent && (
270
- <div className="w-full">{customProfileContent}</div>
271
- )}
272
- </div>
273
-
274
- <ul className="flex flex-col gap-2 mt-2">
275
- {showDeleteConversation && (
276
- <li>
277
- <ActionButton
278
- onClick={handleLeaveConversation}
279
- disabled={isLeaving}
280
- aria-busy={isLeaving}
281
- >
282
- {isLeaving ? (
283
- <SpinnerGapIcon className="h-5 w-5 animate-spin" />
284
- ) : (
285
- <SignOutIcon className="h-5 w-5" />
286
- )}
287
- <span>Delete Conversation</span>
288
- </ActionButton>
289
- </li>
290
- )}
291
- {showBlockParticipant && (
292
- <li>
293
- {isParticipantBlocked ? (
294
- <ActionButton
295
- onClick={handleUnblockUser}
296
- disabled={isUpdatingBlockStatus}
297
- aria-busy={isUpdatingBlockStatus}
298
- >
299
- {isUpdatingBlockStatus ? (
300
- <SpinnerGapIcon className="h-5 w-5 animate-spin" />
301
- ) : (
302
- <ProhibitInsetIcon className="h-5 w-5" />
303
- )}
304
- <span>Unblock</span>
305
- </ActionButton>
306
- ) : (
307
- <ActionButton
308
- onClick={handleBlockUser}
309
- disabled={isUpdatingBlockStatus}
310
- aria-busy={isUpdatingBlockStatus}
311
- >
312
- {isUpdatingBlockStatus ? (
313
- <SpinnerGapIcon className="h-5 w-5 animate-spin" />
314
- ) : (
315
- <ProhibitInsetIcon className="h-5 w-5" />
316
- )}
317
- <span>Block</span>
318
- </ActionButton>
319
- )}
320
- </li>
321
- )}
322
- {showReportParticipant && (
323
- <li>
324
- <ActionButton variant="danger" onClick={handleReportUser}>
325
- <FlagIcon className="h-5 w-5" />
326
- <span>Report</span>
327
- </ActionButton>
328
- </li>
329
- )}
330
- {customChannelActions}
331
- </ul>
332
- </div>
333
- </div>
334
- </dialog>
335
- )
336
- }