@linktr.ee/messaging-react 3.2.0 → 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/dist/{Card-bdnjL_4d.js → Card-BAc2cgtn.js} +3 -3
- package/dist/{Card-bdnjL_4d.js.map → Card-BAc2cgtn.js.map} +1 -1
- package/dist/{Card-CO089n1e.cjs → Card-Cn1cBVnr.cjs} +2 -2
- package/dist/{Card-CO089n1e.cjs.map → Card-Cn1cBVnr.cjs.map} +1 -1
- package/dist/{Card-B5TCecD6.cjs → Card-DAyszUxa.cjs} +2 -2
- package/dist/{Card-B5TCecD6.cjs.map → Card-DAyszUxa.cjs.map} +1 -1
- package/dist/{Card-DQYLHbDI.js → Card-D_2VQScd.js} +2 -2
- package/dist/{Card-DQYLHbDI.js.map → Card-D_2VQScd.js.map} +1 -1
- package/dist/{Card-DTaHgygz.js → Card-D_G8133I.js} +2 -2
- package/dist/{Card-DTaHgygz.js.map → Card-D_G8133I.js.map} +1 -1
- package/dist/{Card-aO1qZWDU.cjs → Card-gYxPXe_W.cjs} +2 -2
- package/dist/{Card-aO1qZWDU.cjs.map → Card-gYxPXe_W.cjs.map} +1 -1
- package/dist/{LockedThumbnail-nsFA3DjA.js → LockedThumbnail-C7tWpOQr.js} +2 -2
- package/dist/{LockedThumbnail-nsFA3DjA.js.map → LockedThumbnail-C7tWpOQr.js.map} +1 -1
- package/dist/{LockedThumbnail-CWVybsBb.cjs → LockedThumbnail-DtOTZl3l.cjs} +2 -2
- package/dist/{LockedThumbnail-CWVybsBb.cjs.map → LockedThumbnail-DtOTZl3l.cjs.map} +1 -1
- package/dist/{index-DJKFVBkP.js → index-C_NFzAB9.js} +1356 -1382
- 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 +20 -15
- package/dist/index.js +1 -1
- package/package.json +3 -3
- package/src/components/ChannelActionsMenu/ChannelActionsMenu.test.tsx +305 -0
- package/src/components/ChannelActionsMenu/index.tsx +221 -0
- package/src/components/ChannelView.stories.tsx +3 -73
- package/src/components/ChannelView.test.tsx +30 -57
- package/src/components/ChannelView.tsx +66 -115
- package/src/hooks/useChannelModerationActions.ts +227 -0
- package/src/types.ts +20 -15
- package/dist/index-BO2VfA-M.cjs +0 -2
- package/dist/index-BO2VfA-M.cjs.map +0 -1
- package/dist/index-DJKFVBkP.js.map +0 -1
- package/src/components/ChannelInfoDialog/ChannelInfoDialog.test.tsx +0 -333
- 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
|
-
}
|