@linktr.ee/messaging-react 1.24.4 → 1.25.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/index.d.ts +11 -1
- package/dist/index.js +860 -827
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/Avatar/Avatar.stories.tsx +7 -0
- package/src/components/Avatar/index.tsx +26 -14
- package/src/components/ChannelInfoDialog/ChannelInfoDialog.test.tsx +259 -0
- package/src/components/ChannelInfoDialog/index.tsx +316 -0
- package/src/components/ChannelList/CustomChannelPreview.test.tsx +57 -5
- package/src/components/ChannelList/CustomChannelPreview.tsx +8 -0
- package/src/components/ChannelView.tsx +12 -330
- package/src/components/MessagingShell/index.tsx +2 -0
- package/src/hooks/useChannelStar.ts +31 -0
- package/src/types.ts +11 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { act } from '@testing-library/react'
|
|
1
2
|
import { Channel, LocalMessage, StreamChat } from 'stream-chat'
|
|
2
3
|
import { describe, expect, it, vi } from 'vitest'
|
|
3
4
|
|
|
@@ -10,10 +11,16 @@ const mockUser = {
|
|
|
10
11
|
name: 'Current User',
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
type MockChannel = Channel & {
|
|
15
|
+
emitMemberUpdated: (member?: { pinned_at?: string | null }) => void
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const createMockChannel = (messages: Partial<LocalMessage>[]): MockChannel => {
|
|
19
|
+
const memberUpdatedListeners = new Set<
|
|
20
|
+
(event: { member?: { pinned_at?: string | null } }) => void
|
|
21
|
+
>()
|
|
22
|
+
|
|
23
|
+
return {
|
|
17
24
|
id: 'channel-1',
|
|
18
25
|
cid: 'messaging:channel-1',
|
|
19
26
|
_client: { userID: mockUser.id } as unknown as StreamChat,
|
|
@@ -26,8 +33,33 @@ const createMockChannel = (
|
|
|
26
33
|
},
|
|
27
34
|
},
|
|
28
35
|
messages: messages as LocalMessage[],
|
|
36
|
+
membership: {},
|
|
29
37
|
},
|
|
30
|
-
|
|
38
|
+
on: vi.fn(
|
|
39
|
+
(
|
|
40
|
+
eventName: string,
|
|
41
|
+
listener: (event: { member?: { pinned_at?: string | null } }) => void
|
|
42
|
+
) => {
|
|
43
|
+
if (eventName === 'member.updated') {
|
|
44
|
+
memberUpdatedListeners.add(listener)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
),
|
|
48
|
+
off: vi.fn(
|
|
49
|
+
(
|
|
50
|
+
eventName: string,
|
|
51
|
+
listener: (event: { member?: { pinned_at?: string | null } }) => void
|
|
52
|
+
) => {
|
|
53
|
+
if (eventName === 'member.updated') {
|
|
54
|
+
memberUpdatedListeners.delete(listener)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
),
|
|
58
|
+
emitMemberUpdated: (member?: { pinned_at?: string | null }) => {
|
|
59
|
+
memberUpdatedListeners.forEach((listener) => listener({ member }))
|
|
60
|
+
},
|
|
61
|
+
} as unknown as MockChannel
|
|
62
|
+
}
|
|
31
63
|
|
|
32
64
|
describe('CustomChannelPreview', () => {
|
|
33
65
|
const defaultProps = {
|
|
@@ -154,4 +186,24 @@ describe('CustomChannelPreview', () => {
|
|
|
154
186
|
|
|
155
187
|
expect(screen.getByText('No messages yet')).toBeInTheDocument()
|
|
156
188
|
})
|
|
189
|
+
|
|
190
|
+
it('updates starred state when the channel membership changes', () => {
|
|
191
|
+
const channel = createMockChannel([])
|
|
192
|
+
|
|
193
|
+
renderWithProviders(
|
|
194
|
+
<CustomChannelPreview {...defaultProps} channel={channel} />
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
expect(
|
|
198
|
+
screen.queryByRole('heading', { name: /starred conversation/i })
|
|
199
|
+
).not.toBeInTheDocument()
|
|
200
|
+
|
|
201
|
+
act(() => {
|
|
202
|
+
channel.emitMemberUpdated({ pinned_at: new Date().toISOString() })
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
expect(
|
|
206
|
+
screen.getByRole('heading', { name: /starred conversation/i })
|
|
207
|
+
).toBeInTheDocument()
|
|
208
|
+
})
|
|
157
209
|
})
|
|
@@ -2,6 +2,7 @@ import classNames from 'classnames'
|
|
|
2
2
|
import React from 'react'
|
|
3
3
|
import { ChannelPreviewUIComponentProps } from 'stream-chat-react'
|
|
4
4
|
|
|
5
|
+
import { useChannelStar } from '../../hooks/useChannelStar'
|
|
5
6
|
import { formatRelativeTime } from '../../utils/formatRelativeTime'
|
|
6
7
|
import { Avatar } from '../Avatar'
|
|
7
8
|
import { isChatbotMessage } from '../CustomMessage/MessageTag'
|
|
@@ -53,6 +54,8 @@ const CustomChannelPreview = React.memo<ChannelPreviewUIComponentProps>(
|
|
|
53
54
|
|
|
54
55
|
const getLastMessageText = () => {
|
|
55
56
|
if (lastMessage?.text) return lastMessage.text
|
|
57
|
+
const isTip = lastMessage?.metadata?.custom_type === 'MESSAGE_TIP'
|
|
58
|
+
if (isTip) return '💵 Sent a tip'
|
|
56
59
|
|
|
57
60
|
const attachment = lastMessage?.attachments?.[0]
|
|
58
61
|
if (attachment) {
|
|
@@ -83,6 +86,7 @@ const CustomChannelPreview = React.memo<ChannelPreviewUIComponentProps>(
|
|
|
83
86
|
const messagePreview = renderMessagePreview
|
|
84
87
|
? renderMessagePreview(lastMessage, lastMessageText)
|
|
85
88
|
: `${isLastMessageFromChatbot ? '✨ ' : ''}${lastMessageText}`
|
|
89
|
+
const isChannelStarred = useChannelStar(channel)
|
|
86
90
|
|
|
87
91
|
// Use the unread prop passed by Stream Chat (reactive and updates automatically)
|
|
88
92
|
const unreadCount = unread ?? 0
|
|
@@ -118,6 +122,7 @@ const CustomChannelPreview = React.memo<ChannelPreviewUIComponentProps>(
|
|
|
118
122
|
name={participantName}
|
|
119
123
|
image={participantImage}
|
|
120
124
|
size={44}
|
|
125
|
+
starred={isChannelStarred}
|
|
121
126
|
className="[&_.avatar-fallback]:group-hover:bg-[#eeeeee]"
|
|
122
127
|
/>
|
|
123
128
|
|
|
@@ -131,6 +136,9 @@ const CustomChannelPreview = React.memo<ChannelPreviewUIComponentProps>(
|
|
|
131
136
|
isSelected ? 'text-primary' : 'text-charcoal'
|
|
132
137
|
)}
|
|
133
138
|
>
|
|
139
|
+
{isChannelStarred && (
|
|
140
|
+
<span className="sr-only">Starred conversation. </span>
|
|
141
|
+
)}
|
|
134
142
|
{participantName}
|
|
135
143
|
</h3>
|
|
136
144
|
{lastMessageTime && (
|
|
@@ -1,19 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ArrowLeftIcon,
|
|
3
3
|
DotsThreeIcon,
|
|
4
|
-
FlagIcon,
|
|
5
|
-
ProhibitInsetIcon,
|
|
6
|
-
SignOutIcon,
|
|
7
|
-
SpinnerGapIcon,
|
|
8
4
|
StarIcon,
|
|
9
5
|
} from '@phosphor-icons/react'
|
|
10
6
|
import classNames from 'classnames'
|
|
11
|
-
import React, {
|
|
12
|
-
import {
|
|
13
|
-
Channel as ChannelType,
|
|
14
|
-
ChannelMemberResponse,
|
|
15
|
-
Event,
|
|
16
|
-
} from 'stream-chat'
|
|
7
|
+
import React, { useCallback, useRef } from 'react'
|
|
8
|
+
import { Channel as ChannelType } from 'stream-chat'
|
|
17
9
|
import {
|
|
18
10
|
Channel,
|
|
19
11
|
Window,
|
|
@@ -24,12 +16,11 @@ import {
|
|
|
24
16
|
MessageUIComponentProps,
|
|
25
17
|
} from 'stream-chat-react'
|
|
26
18
|
|
|
27
|
-
import {
|
|
19
|
+
import { useChannelStar } from '../hooks/useChannelStar'
|
|
28
20
|
import type { ChannelViewProps } from '../types'
|
|
29
21
|
|
|
30
|
-
import ActionButton from './ActionButton'
|
|
31
22
|
import { Avatar } from './Avatar'
|
|
32
|
-
import {
|
|
23
|
+
import { ChannelInfoDialog } from './ChannelInfoDialog'
|
|
33
24
|
import { CustomDateSeparator } from './CustomDateSeparator'
|
|
34
25
|
import { CustomMessage } from './CustomMessage'
|
|
35
26
|
import { CustomMessageInput } from './CustomMessageInput'
|
|
@@ -37,17 +28,6 @@ import { CustomSystemMessage } from './CustomSystemMessage'
|
|
|
37
28
|
import { ChannelEmptyState } from './MessagingShell/ChannelEmptyState'
|
|
38
29
|
import { LoadingState } from './MessagingShell/LoadingState'
|
|
39
30
|
|
|
40
|
-
// Custom user type with email and username
|
|
41
|
-
type CustomUser = {
|
|
42
|
-
email?: string
|
|
43
|
-
username?: string
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Blocked user from Stream Chat API
|
|
47
|
-
type BlockedUser = {
|
|
48
|
-
blocked_user_id: string
|
|
49
|
-
}
|
|
50
|
-
|
|
51
31
|
const ICON_BTN_CLASS =
|
|
52
32
|
'size-10 rounded-full bg-[#F1F0EE] hover:bg-[#E5E4E1] flex items-center justify-center transition-colors duration-150 focus-ring'
|
|
53
33
|
|
|
@@ -80,26 +60,7 @@ const CustomChannelHeader: React.FC<{
|
|
|
80
60
|
const participantName =
|
|
81
61
|
participant?.user?.name || participant?.user?.id || 'Unknown member'
|
|
82
62
|
const participantImage = participant?.user?.image
|
|
83
|
-
|
|
84
|
-
const [isStarred, setIsStarred] = useState(
|
|
85
|
-
!!channel.state.membership?.pinned_at
|
|
86
|
-
)
|
|
87
|
-
|
|
88
|
-
useEffect(() => {
|
|
89
|
-
const handleMemberUpdate = (event: Event) => {
|
|
90
|
-
setIsStarred(
|
|
91
|
-
event?.member
|
|
92
|
-
? !!event.member.pinned_at
|
|
93
|
-
: !!channel.state.membership?.pinned_at
|
|
94
|
-
)
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
channel.on('member.updated', handleMemberUpdate)
|
|
98
|
-
|
|
99
|
-
return () => {
|
|
100
|
-
channel.off('member.updated', handleMemberUpdate)
|
|
101
|
-
}
|
|
102
|
-
}, [channel])
|
|
63
|
+
const isStarred = useChannelStar(channel)
|
|
103
64
|
|
|
104
65
|
const handleStarClick = async () => {
|
|
105
66
|
try {
|
|
@@ -136,6 +97,7 @@ const CustomChannelHeader: React.FC<{
|
|
|
136
97
|
id={participant?.user?.id || channel.id || 'unknown'}
|
|
137
98
|
name={participantName}
|
|
138
99
|
image={participantImage}
|
|
100
|
+
starred={isStarred}
|
|
139
101
|
size={40}
|
|
140
102
|
/>
|
|
141
103
|
<h1 className="text-xs font-medium text-black/90">
|
|
@@ -188,6 +150,7 @@ const CustomChannelHeader: React.FC<{
|
|
|
188
150
|
id={participant?.user?.id || channel.id || 'unknown'}
|
|
189
151
|
name={participantName}
|
|
190
152
|
image={participantImage}
|
|
153
|
+
starred={isStarred}
|
|
191
154
|
size={40}
|
|
192
155
|
/>
|
|
193
156
|
<div className="min-w-0">
|
|
@@ -231,292 +194,6 @@ const CustomChannelHeader: React.FC<{
|
|
|
231
194
|
)
|
|
232
195
|
}
|
|
233
196
|
|
|
234
|
-
/**
|
|
235
|
-
* Channel info dialog (matching original implementation)
|
|
236
|
-
*/
|
|
237
|
-
const ChannelInfoDialog: React.FC<{
|
|
238
|
-
dialogRef: React.RefObject<HTMLDialogElement>
|
|
239
|
-
onClose: () => void
|
|
240
|
-
participant: ChannelMemberResponse | undefined
|
|
241
|
-
channel: ChannelType
|
|
242
|
-
followerStatusLabel?: string
|
|
243
|
-
onLeaveConversation?: (channel: ChannelType) => void
|
|
244
|
-
onBlockParticipant?: (participantId?: string) => void
|
|
245
|
-
showDeleteConversation?: boolean
|
|
246
|
-
onDeleteConversationClick?: () => void
|
|
247
|
-
onBlockParticipantClick?: () => void
|
|
248
|
-
onReportParticipantClick?: () => void
|
|
249
|
-
customChannelActions?: React.ReactNode
|
|
250
|
-
}> = ({
|
|
251
|
-
dialogRef,
|
|
252
|
-
onClose,
|
|
253
|
-
participant,
|
|
254
|
-
channel,
|
|
255
|
-
followerStatusLabel,
|
|
256
|
-
onLeaveConversation,
|
|
257
|
-
onBlockParticipant,
|
|
258
|
-
showDeleteConversation = true,
|
|
259
|
-
onDeleteConversationClick,
|
|
260
|
-
onBlockParticipantClick,
|
|
261
|
-
onReportParticipantClick,
|
|
262
|
-
customChannelActions,
|
|
263
|
-
}) => {
|
|
264
|
-
const { service, debug } = useMessagingContext()
|
|
265
|
-
const [isParticipantBlocked, setIsParticipantBlocked] = useState(false)
|
|
266
|
-
const [isLeaving, setIsLeaving] = useState(false)
|
|
267
|
-
const [isUpdatingBlockStatus, setIsUpdatingBlockStatus] = useState(false)
|
|
268
|
-
|
|
269
|
-
// Check if participant is blocked when participant changes
|
|
270
|
-
const checkIsParticipantBlocked = useCallback(async () => {
|
|
271
|
-
if (!service || !participant?.user?.id) return
|
|
272
|
-
|
|
273
|
-
try {
|
|
274
|
-
const blockedUsers = await service.getBlockedUsers()
|
|
275
|
-
const isBlocked = blockedUsers.some(
|
|
276
|
-
(user: BlockedUser) => user.blocked_user_id === participant?.user?.id
|
|
277
|
-
)
|
|
278
|
-
setIsParticipantBlocked(isBlocked)
|
|
279
|
-
} catch (error) {
|
|
280
|
-
console.error(
|
|
281
|
-
'[ChannelInfoDialog] Failed to check blocked status:',
|
|
282
|
-
error
|
|
283
|
-
)
|
|
284
|
-
}
|
|
285
|
-
}, [service, participant?.user?.id])
|
|
286
|
-
|
|
287
|
-
useEffect(() => {
|
|
288
|
-
checkIsParticipantBlocked()
|
|
289
|
-
}, [checkIsParticipantBlocked])
|
|
290
|
-
|
|
291
|
-
const handleLeaveConversation = async () => {
|
|
292
|
-
if (isLeaving) return
|
|
293
|
-
|
|
294
|
-
// Fire analytics callback before action
|
|
295
|
-
onDeleteConversationClick?.()
|
|
296
|
-
|
|
297
|
-
if (debug) {
|
|
298
|
-
console.log('[ChannelInfoDialog] Leave conversation', channel.cid)
|
|
299
|
-
}
|
|
300
|
-
setIsLeaving(true)
|
|
301
|
-
|
|
302
|
-
try {
|
|
303
|
-
const actingUserId = channel._client?.userID ?? null
|
|
304
|
-
await channel.hide(actingUserId, false)
|
|
305
|
-
|
|
306
|
-
if (onLeaveConversation) {
|
|
307
|
-
await onLeaveConversation(channel)
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
onClose()
|
|
311
|
-
} catch (error) {
|
|
312
|
-
console.error('[ChannelInfoDialog] Failed to leave conversation', error)
|
|
313
|
-
} finally {
|
|
314
|
-
setIsLeaving(false)
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
const handleBlockUser = async () => {
|
|
319
|
-
if (isUpdatingBlockStatus || !service) return
|
|
320
|
-
|
|
321
|
-
// Fire analytics callback before action
|
|
322
|
-
onBlockParticipantClick?.()
|
|
323
|
-
|
|
324
|
-
if (debug) {
|
|
325
|
-
console.log('[ChannelInfoDialog] Block member', participant?.user?.id)
|
|
326
|
-
}
|
|
327
|
-
setIsUpdatingBlockStatus(true)
|
|
328
|
-
|
|
329
|
-
try {
|
|
330
|
-
await service.blockUser(participant?.user?.id)
|
|
331
|
-
|
|
332
|
-
if (onBlockParticipant) {
|
|
333
|
-
await onBlockParticipant(participant?.user?.id)
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
onClose()
|
|
337
|
-
} catch (error) {
|
|
338
|
-
console.error('[ChannelInfoDialog] Failed to block member', error)
|
|
339
|
-
} finally {
|
|
340
|
-
setIsUpdatingBlockStatus(false)
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
const handleUnblockUser = async () => {
|
|
345
|
-
if (isUpdatingBlockStatus || !service) return
|
|
346
|
-
|
|
347
|
-
// Fire analytics callback before action
|
|
348
|
-
onBlockParticipantClick?.()
|
|
349
|
-
|
|
350
|
-
if (debug) {
|
|
351
|
-
console.log('[ChannelInfoDialog] Unblock member', participant?.user?.id)
|
|
352
|
-
}
|
|
353
|
-
setIsUpdatingBlockStatus(true)
|
|
354
|
-
|
|
355
|
-
try {
|
|
356
|
-
await service.unBlockUser(participant?.user?.id)
|
|
357
|
-
|
|
358
|
-
if (onBlockParticipant) {
|
|
359
|
-
await onBlockParticipant(participant?.user?.id)
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
onClose()
|
|
363
|
-
} catch (error) {
|
|
364
|
-
console.error('[ChannelInfoDialog] Failed to unblock member', error)
|
|
365
|
-
} finally {
|
|
366
|
-
setIsUpdatingBlockStatus(false)
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
const handleReportUser = () => {
|
|
371
|
-
// Fire analytics callback before action
|
|
372
|
-
onReportParticipantClick?.()
|
|
373
|
-
|
|
374
|
-
onClose()
|
|
375
|
-
window.open(
|
|
376
|
-
'https://linktr.ee/s/about/trust-center/report',
|
|
377
|
-
'_blank',
|
|
378
|
-
'noopener,noreferrer'
|
|
379
|
-
)
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
if (!participant) return null
|
|
383
|
-
|
|
384
|
-
const participantName =
|
|
385
|
-
participant.user?.name || participant.user?.id || 'Unknown member'
|
|
386
|
-
const participantImage = participant.user?.image
|
|
387
|
-
const participantEmail = (participant.user as CustomUser)?.email
|
|
388
|
-
const participantUsername = (participant.user as CustomUser)?.username
|
|
389
|
-
const participantSecondary = participantEmail
|
|
390
|
-
? participantEmail
|
|
391
|
-
: participantUsername
|
|
392
|
-
? `linktr.ee/${participantUsername}`
|
|
393
|
-
: undefined
|
|
394
|
-
const participantId = participant.user?.id || 'unknown'
|
|
395
|
-
|
|
396
|
-
return (
|
|
397
|
-
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions
|
|
398
|
-
<dialog
|
|
399
|
-
ref={dialogRef}
|
|
400
|
-
className="mes-dialog group"
|
|
401
|
-
onClose={onClose}
|
|
402
|
-
onClick={(e) => {
|
|
403
|
-
if (e.target === dialogRef.current) {
|
|
404
|
-
onClose()
|
|
405
|
-
}
|
|
406
|
-
}}
|
|
407
|
-
>
|
|
408
|
-
<div className="ml-auto flex h-full w-full flex-col bg-white shadow-none transition-shadow duration-200 group-open:shadow-max-elevation-light">
|
|
409
|
-
<div className="flex items-center justify-between border-b border-sand px-4 py-3">
|
|
410
|
-
<h2 className="text-base font-semibold text-charcoal">Chat info</h2>
|
|
411
|
-
<CloseButton onClick={onClose} />
|
|
412
|
-
</div>
|
|
413
|
-
|
|
414
|
-
<div className="flex-1 px-2 overflow-y-auto w-full">
|
|
415
|
-
<div
|
|
416
|
-
className="flex flex-col items-center gap-3 self-stretch px-4 py-2 mt-6 rounded-lg border border-black/[0.04]"
|
|
417
|
-
style={{ backgroundColor: '#FBFAF9' }}
|
|
418
|
-
>
|
|
419
|
-
<div className="flex items-center gap-3 w-full">
|
|
420
|
-
<Avatar
|
|
421
|
-
id={participantId}
|
|
422
|
-
name={participantName}
|
|
423
|
-
image={participantImage}
|
|
424
|
-
size={88}
|
|
425
|
-
shape="circle"
|
|
426
|
-
/>
|
|
427
|
-
<div className="flex flex-col min-w-0 flex-1">
|
|
428
|
-
<p className="truncate text-base font-semibold text-charcoal">
|
|
429
|
-
{participantName}
|
|
430
|
-
</p>
|
|
431
|
-
{participantSecondary && (
|
|
432
|
-
<p className="truncate text-sm text-[#00000055]">
|
|
433
|
-
{participantSecondary}
|
|
434
|
-
</p>
|
|
435
|
-
)}
|
|
436
|
-
{followerStatusLabel && (
|
|
437
|
-
<span
|
|
438
|
-
className="mt-1 rounded-full text-xs font-normal w-fit"
|
|
439
|
-
style={{
|
|
440
|
-
padding: '4px 8px',
|
|
441
|
-
backgroundColor:
|
|
442
|
-
followerStatusLabel === 'Subscribed to you'
|
|
443
|
-
? '#DCFCE7'
|
|
444
|
-
: '#F5F5F4',
|
|
445
|
-
color:
|
|
446
|
-
followerStatusLabel === 'Subscribed to you'
|
|
447
|
-
? '#008236'
|
|
448
|
-
: '#78716C',
|
|
449
|
-
lineHeight: '133.333%',
|
|
450
|
-
letterSpacing: '0.21px',
|
|
451
|
-
}}
|
|
452
|
-
>
|
|
453
|
-
{followerStatusLabel}
|
|
454
|
-
</span>
|
|
455
|
-
)}
|
|
456
|
-
</div>
|
|
457
|
-
</div>
|
|
458
|
-
</div>
|
|
459
|
-
|
|
460
|
-
<ul className="flex flex-col gap-2 mt-2">
|
|
461
|
-
{showDeleteConversation && (
|
|
462
|
-
<li>
|
|
463
|
-
<ActionButton
|
|
464
|
-
onClick={handleLeaveConversation}
|
|
465
|
-
disabled={isLeaving}
|
|
466
|
-
aria-busy={isLeaving}
|
|
467
|
-
>
|
|
468
|
-
{isLeaving ? (
|
|
469
|
-
<SpinnerGapIcon className="h-5 w-5 animate-spin" />
|
|
470
|
-
) : (
|
|
471
|
-
<SignOutIcon className="h-5 w-5" />
|
|
472
|
-
)}
|
|
473
|
-
<span>Delete Conversation</span>
|
|
474
|
-
</ActionButton>
|
|
475
|
-
</li>
|
|
476
|
-
)}
|
|
477
|
-
<li>
|
|
478
|
-
{isParticipantBlocked ? (
|
|
479
|
-
<ActionButton
|
|
480
|
-
onClick={handleUnblockUser}
|
|
481
|
-
disabled={isUpdatingBlockStatus}
|
|
482
|
-
aria-busy={isUpdatingBlockStatus}
|
|
483
|
-
>
|
|
484
|
-
{isUpdatingBlockStatus ? (
|
|
485
|
-
<SpinnerGapIcon className="h-5 w-5 animate-spin" />
|
|
486
|
-
) : (
|
|
487
|
-
<ProhibitInsetIcon className="h-5 w-5" />
|
|
488
|
-
)}
|
|
489
|
-
<span>Unblock</span>
|
|
490
|
-
</ActionButton>
|
|
491
|
-
) : (
|
|
492
|
-
<ActionButton
|
|
493
|
-
onClick={handleBlockUser}
|
|
494
|
-
disabled={isUpdatingBlockStatus}
|
|
495
|
-
aria-busy={isUpdatingBlockStatus}
|
|
496
|
-
>
|
|
497
|
-
{isUpdatingBlockStatus ? (
|
|
498
|
-
<SpinnerGapIcon className="h-5 w-5 animate-spin" />
|
|
499
|
-
) : (
|
|
500
|
-
<ProhibitInsetIcon className="h-5 w-5" />
|
|
501
|
-
)}
|
|
502
|
-
<span>Block</span>
|
|
503
|
-
</ActionButton>
|
|
504
|
-
)}
|
|
505
|
-
</li>
|
|
506
|
-
<li>
|
|
507
|
-
<ActionButton variant="danger" onClick={handleReportUser}>
|
|
508
|
-
<FlagIcon className="h-5 w-5" />
|
|
509
|
-
<span>Report</span>
|
|
510
|
-
</ActionButton>
|
|
511
|
-
</li>
|
|
512
|
-
{customChannelActions}
|
|
513
|
-
</ul>
|
|
514
|
-
</div>
|
|
515
|
-
</div>
|
|
516
|
-
</dialog>
|
|
517
|
-
)
|
|
518
|
-
}
|
|
519
|
-
|
|
520
197
|
/**
|
|
521
198
|
* Inner component that has access to channel context
|
|
522
199
|
*/
|
|
@@ -535,6 +212,7 @@ const ChannelViewInner: React.FC<{
|
|
|
535
212
|
showStarButton?: boolean
|
|
536
213
|
chatbotVotingEnabled?: boolean
|
|
537
214
|
renderChannelBanner?: () => React.ReactNode
|
|
215
|
+
customProfileContent?: React.ReactNode
|
|
538
216
|
customChannelActions?: React.ReactNode
|
|
539
217
|
renderMessage?: (
|
|
540
218
|
messageNode: React.ReactElement,
|
|
@@ -554,6 +232,7 @@ const ChannelViewInner: React.FC<{
|
|
|
554
232
|
showStarButton = false,
|
|
555
233
|
chatbotVotingEnabled = false,
|
|
556
234
|
renderChannelBanner,
|
|
235
|
+
customProfileContent,
|
|
557
236
|
customChannelActions,
|
|
558
237
|
renderMessage,
|
|
559
238
|
}) => {
|
|
@@ -664,6 +343,7 @@ const ChannelViewInner: React.FC<{
|
|
|
664
343
|
onDeleteConversationClick={onDeleteConversationClick}
|
|
665
344
|
onBlockParticipantClick={onBlockParticipantClick}
|
|
666
345
|
onReportParticipantClick={onReportParticipantClick}
|
|
346
|
+
customProfileContent={customProfileContent}
|
|
667
347
|
customChannelActions={customChannelActions}
|
|
668
348
|
/>
|
|
669
349
|
</>
|
|
@@ -694,6 +374,7 @@ export const ChannelView = React.memo<ChannelViewProps>(
|
|
|
694
374
|
showStarButton = false,
|
|
695
375
|
chatbotVotingEnabled = false,
|
|
696
376
|
renderChannelBanner,
|
|
377
|
+
customProfileContent,
|
|
697
378
|
customChannelActions,
|
|
698
379
|
renderMessage,
|
|
699
380
|
}) => {
|
|
@@ -771,6 +452,7 @@ export const ChannelView = React.memo<ChannelViewProps>(
|
|
|
771
452
|
showStarButton={showStarButton}
|
|
772
453
|
chatbotVotingEnabled={chatbotVotingEnabled}
|
|
773
454
|
renderChannelBanner={renderChannelBanner}
|
|
455
|
+
customProfileContent={customProfileContent}
|
|
774
456
|
customChannelActions={customChannelActions}
|
|
775
457
|
renderMessage={renderMessage}
|
|
776
458
|
/>
|
|
@@ -38,6 +38,7 @@ export const MessagingShell: React.FC<MessagingShellProps> = ({
|
|
|
38
38
|
chatbotVotingEnabled = false,
|
|
39
39
|
renderMessagePreview,
|
|
40
40
|
renderChannelBanner,
|
|
41
|
+
customProfileContent,
|
|
41
42
|
customChannelActions,
|
|
42
43
|
renderMessage,
|
|
43
44
|
}) => {
|
|
@@ -499,6 +500,7 @@ export const MessagingShell: React.FC<MessagingShellProps> = ({
|
|
|
499
500
|
onMessageSent={onMessageSent}
|
|
500
501
|
showStarButton={showStarButton}
|
|
501
502
|
chatbotVotingEnabled={chatbotVotingEnabled}
|
|
503
|
+
customProfileContent={customProfileContent}
|
|
502
504
|
customChannelActions={customChannelActions}
|
|
503
505
|
renderMessage={renderMessage}
|
|
504
506
|
/>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react'
|
|
2
|
+
import { Channel, Event } from 'stream-chat'
|
|
3
|
+
|
|
4
|
+
export const useChannelStar = (channel?: Channel) => {
|
|
5
|
+
const [isChannelStarred, setIsChannelStarred] = useState(
|
|
6
|
+
!!channel?.state?.membership?.pinned_at
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
if (!channel) {
|
|
11
|
+
setIsChannelStarred(false)
|
|
12
|
+
return
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
setIsChannelStarred(!!channel.state.membership?.pinned_at)
|
|
16
|
+
|
|
17
|
+
const handleMemberUpdate = (event: Event) => {
|
|
18
|
+
setIsChannelStarred(
|
|
19
|
+
event?.member ? !!event.member.pinned_at : !!channel.state.membership?.pinned_at
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
channel.on('member.updated', handleMemberUpdate)
|
|
24
|
+
|
|
25
|
+
return () => {
|
|
26
|
+
channel.off('member.updated', handleMemberUpdate)
|
|
27
|
+
}
|
|
28
|
+
}, [channel])
|
|
29
|
+
|
|
30
|
+
return isChannelStarred
|
|
31
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -185,6 +185,16 @@ export interface ChannelViewProps {
|
|
|
185
185
|
*/
|
|
186
186
|
renderChannelBanner?: () => React.ReactNode
|
|
187
187
|
|
|
188
|
+
/**
|
|
189
|
+
* Custom content rendered below the participant name and contact details
|
|
190
|
+
* in the channel info dialog profile card.
|
|
191
|
+
* Useful for badges (e.g. follower status), metadata, or any extra info.
|
|
192
|
+
*
|
|
193
|
+
* @example
|
|
194
|
+
* customProfileContent={<SubscriptionBadge isFollower={channel.data?.isFollower} />}
|
|
195
|
+
*/
|
|
196
|
+
customProfileContent?: React.ReactNode
|
|
197
|
+
|
|
188
198
|
/**
|
|
189
199
|
* Custom actions rendered at the bottom of the channel info dialog
|
|
190
200
|
* (below Delete Conversation, Block/Unblock, Report).
|
|
@@ -227,6 +237,7 @@ export type ChannelViewPassthroughProps = Pick<
|
|
|
227
237
|
| 'showStarButton'
|
|
228
238
|
| 'chatbotVotingEnabled'
|
|
229
239
|
| 'renderChannelBanner'
|
|
240
|
+
| 'customProfileContent'
|
|
230
241
|
| 'customChannelActions'
|
|
231
242
|
| 'renderMessage'
|
|
232
243
|
>
|