@linktr.ee/messaging-react 1.11.3 → 1.11.5-rc-1765005296
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 +2 -2
- package/dist/index.js +690 -676
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/ChannelList/CustomChannelPreview.tsx +109 -106
- package/src/components/ChannelList/index.tsx +55 -66
- package/src/components/ChannelView.tsx +37 -41
- package/src/components/MessagingShell/ChannelEmptyState.tsx +1 -12
- package/src/components/MessagingShell/EmptyState.tsx +4 -4
- package/src/components/MessagingShell/ErrorState.tsx +8 -5
- package/src/components/MessagingShell/LoadingState.tsx +5 -2
- package/src/components/MessagingShell/index.tsx +20 -6
package/package.json
CHANGED
|
@@ -6,123 +6,126 @@ import { ChannelPreviewUIComponentProps } from 'stream-chat-react'
|
|
|
6
6
|
import { formatRelativeTime } from '../../utils/formatRelativeTime'
|
|
7
7
|
import { Avatar } from '../Avatar'
|
|
8
8
|
|
|
9
|
+
type CustomChannelPreviewProps = ChannelPreviewUIComponentProps & {
|
|
10
|
+
selectedChannel?: Channel | null
|
|
11
|
+
onChannelSelect: (channel: Channel) => void
|
|
12
|
+
debug?: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
9
15
|
/**
|
|
10
16
|
* Custom channel preview that handles selection
|
|
11
17
|
*/
|
|
12
|
-
const CustomChannelPreview
|
|
13
|
-
|
|
14
|
-
selectedChannel
|
|
15
|
-
onChannelSelect: (channel: Channel) => void
|
|
16
|
-
debug?: boolean
|
|
17
|
-
}
|
|
18
|
-
> = ({ channel, selectedChannel, onChannelSelect, debug = false, unread }) => {
|
|
19
|
-
const isSelected = selectedChannel?.id === channel?.id
|
|
18
|
+
const CustomChannelPreview = React.memo<CustomChannelPreviewProps>(
|
|
19
|
+
({ channel, selectedChannel, onChannelSelect, debug = false, unread }) => {
|
|
20
|
+
const isSelected = selectedChannel?.id === channel?.id
|
|
20
21
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
const handleClick = () => {
|
|
23
|
+
if (channel) {
|
|
24
|
+
onChannelSelect(channel)
|
|
25
|
+
}
|
|
24
26
|
}
|
|
25
|
-
}
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
// Get last message and format timestamp
|
|
36
|
-
const lastMessage =
|
|
37
|
-
channel?.state?.messages?.[channel.state.messages.length - 1]
|
|
38
|
-
|
|
39
|
-
// Fallback order: text -> attachment URL -> "No messages yet"
|
|
40
|
-
const getLastMessageText = () => {
|
|
41
|
-
if (lastMessage?.text) return lastMessage.text
|
|
42
|
-
|
|
43
|
-
const attachment = lastMessage?.attachments?.[0]
|
|
44
|
-
if (attachment?.asset_url) return attachment.asset_url
|
|
45
|
-
if (attachment?.image_url) return attachment.image_url
|
|
46
|
-
if (attachment?.og_scrape_url) return attachment.og_scrape_url
|
|
47
|
-
if (attachment?.thumb_url) return attachment.thumb_url
|
|
48
|
-
|
|
49
|
-
return 'No messages yet'
|
|
50
|
-
}
|
|
28
|
+
// Get participant info
|
|
29
|
+
const members = Object.values(channel?.state?.members || {})
|
|
30
|
+
const participant = members.find(
|
|
31
|
+
(member) => member.user?.id && member.user.id !== channel?._client?.userID
|
|
32
|
+
)
|
|
33
|
+
const participantName = participant?.user?.name || 'Conversation'
|
|
34
|
+
const participantImage = participant?.user?.image
|
|
51
35
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
}
|
|
36
|
+
// Get last message and format timestamp
|
|
37
|
+
const lastMessage =
|
|
38
|
+
channel?.state?.messages?.[channel.state.messages.length - 1]
|
|
39
|
+
|
|
40
|
+
// Fallback order: text -> attachment URL -> "No messages yet"
|
|
41
|
+
const getLastMessageText = () => {
|
|
42
|
+
if (lastMessage?.text) return lastMessage.text
|
|
43
|
+
|
|
44
|
+
const attachment = lastMessage?.attachments?.[0]
|
|
45
|
+
if (attachment?.asset_url) return attachment.asset_url
|
|
46
|
+
if (attachment?.image_url) return attachment.image_url
|
|
47
|
+
if (attachment?.og_scrape_url) return attachment.og_scrape_url
|
|
48
|
+
if (attachment?.thumb_url) return attachment.thumb_url
|
|
49
|
+
|
|
50
|
+
return 'No messages yet'
|
|
51
|
+
}
|
|
69
52
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
{
|
|
93
|
-
|
|
94
|
-
{
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
53
|
+
const lastMessageText = getLastMessageText()
|
|
54
|
+
const lastMessageTime = lastMessage?.created_at
|
|
55
|
+
? formatRelativeTime(new Date(lastMessage.created_at))
|
|
56
|
+
: ''
|
|
57
|
+
|
|
58
|
+
// Use the unread prop passed by Stream Chat (reactive and updates automatically)
|
|
59
|
+
const unreadCount = unread ?? 0
|
|
60
|
+
|
|
61
|
+
if (debug) {
|
|
62
|
+
console.log('📺 [ChannelList] 📋 CHANNEL PREVIEW RENDER', {
|
|
63
|
+
channelId: channel?.id,
|
|
64
|
+
isSelected,
|
|
65
|
+
participantName,
|
|
66
|
+
unreadCount,
|
|
67
|
+
hasTimestamp: !!lastMessageTime,
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<button
|
|
73
|
+
type="button"
|
|
74
|
+
onClick={handleClick}
|
|
75
|
+
className={classNames(
|
|
76
|
+
'group w-full px-4 py-3 transition-colors text-left max-w-full overflow-hidden focus-ring',
|
|
77
|
+
{
|
|
78
|
+
'bg-primary-alt/10 border-l-4 border-l-primary': isSelected,
|
|
79
|
+
'hover:bg-sand': !isSelected,
|
|
80
|
+
}
|
|
81
|
+
)}
|
|
82
|
+
>
|
|
83
|
+
<div className="flex items-start gap-3">
|
|
84
|
+
{/* Avatar */}
|
|
85
|
+
<Avatar
|
|
86
|
+
id={participant?.user?.id || channel.id || 'unknown'}
|
|
87
|
+
name={participantName}
|
|
88
|
+
image={participantImage}
|
|
89
|
+
size={44}
|
|
90
|
+
className="[&_.avatar-fallback]:group-hover:bg-[#eeeeee]"
|
|
91
|
+
/>
|
|
92
|
+
|
|
93
|
+
{/* Content column */}
|
|
94
|
+
<div className="flex-1 min-w-0 flex flex-col gap-1">
|
|
95
|
+
{/* Name and timestamp row */}
|
|
96
|
+
<div className="flex items-center justify-between gap-2">
|
|
97
|
+
<h3
|
|
98
|
+
className={classNames(
|
|
99
|
+
'text-sm font-medium truncate',
|
|
100
|
+
isSelected ? 'text-primary' : 'text-charcoal'
|
|
101
|
+
)}
|
|
102
|
+
>
|
|
103
|
+
{participantName}
|
|
104
|
+
</h3>
|
|
105
|
+
{lastMessageTime && (
|
|
106
|
+
<span className="text-xs text-stone flex-shrink-0">
|
|
107
|
+
{lastMessageTime}
|
|
108
|
+
</span>
|
|
100
109
|
)}
|
|
101
|
-
>
|
|
102
|
-
{participantName}
|
|
103
|
-
</h3>
|
|
104
|
-
{lastMessageTime && (
|
|
105
|
-
<span className="text-xs text-stone flex-shrink-0">
|
|
106
|
-
{lastMessageTime}
|
|
107
|
-
</span>
|
|
108
|
-
)}
|
|
109
|
-
</div>
|
|
110
|
+
</div>
|
|
110
111
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
112
|
+
{/* Message and unread badge row */}
|
|
113
|
+
<div className="flex items-center justify-between gap-2 min-w-0">
|
|
114
|
+
<p className="text-xs text-stone mr-2 flex-1 line-clamp-2">
|
|
115
|
+
{lastMessageText}
|
|
116
|
+
</p>
|
|
117
|
+
{unreadCount > 0 && (
|
|
118
|
+
<span className="bg-[#7f22fe] text-white text-xs px-2 py-0.5 rounded-full min-w-[20px] text-center flex-shrink-0">
|
|
119
|
+
{unreadCount > 99 ? '99+' : unreadCount}
|
|
120
|
+
</span>
|
|
121
|
+
)}
|
|
122
|
+
</div>
|
|
121
123
|
</div>
|
|
122
124
|
</div>
|
|
123
|
-
</
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
125
|
+
</button>
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
)
|
|
127
129
|
|
|
128
130
|
export default CustomChannelPreview
|
|
131
|
+
CustomChannelPreview.displayName = 'CustomChannelPreview'
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import classNames from 'classnames'
|
|
2
2
|
import React from 'react'
|
|
3
|
+
import type { ChannelPreviewUIComponentProps } from 'stream-chat-react'
|
|
3
4
|
import { ChannelList as StreamChannelList } from 'stream-chat-react'
|
|
4
5
|
|
|
5
6
|
import { useMessagingContext } from '../../providers/MessagingProvider'
|
|
@@ -10,74 +11,62 @@ import CustomChannelPreview from './CustomChannelPreview'
|
|
|
10
11
|
/**
|
|
11
12
|
* Channel list component with customizable header and actions
|
|
12
13
|
*/
|
|
13
|
-
export const ChannelList
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
14
|
+
export const ChannelList = React.memo<ChannelListProps>(
|
|
15
|
+
({
|
|
16
|
+
onChannelSelect,
|
|
17
|
+
selectedChannel,
|
|
18
|
+
filters,
|
|
19
|
+
className,
|
|
20
|
+
customEmptyStateIndicator,
|
|
21
|
+
}) => {
|
|
22
|
+
// Track renders
|
|
23
|
+
const renderCountRef = React.useRef(0)
|
|
24
|
+
renderCountRef.current++
|
|
23
25
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
+
// Get debug flag from context
|
|
27
|
+
const { debug = false } = useMessagingContext()
|
|
26
28
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return (
|
|
36
|
-
<div
|
|
37
|
-
className={classNames(
|
|
38
|
-
'messaging-channel-list h-full flex flex-col min-w-0 overflow-hidden',
|
|
39
|
-
className
|
|
40
|
-
)}
|
|
41
|
-
>
|
|
42
|
-
{/* Channel List */}
|
|
43
|
-
<div className="flex-1 overflow-hidden min-w-0">
|
|
44
|
-
{(() => {
|
|
45
|
-
if (debug) {
|
|
46
|
-
console.log('📺 [ChannelList] 🎬 RENDERING STREAM CHANNEL LIST', {
|
|
47
|
-
renderCount: renderCountRef.current,
|
|
48
|
-
filters,
|
|
49
|
-
})
|
|
50
|
-
}
|
|
29
|
+
if (debug) {
|
|
30
|
+
console.log('📺 [ChannelList] 🔄 RENDER START', {
|
|
31
|
+
renderCount: renderCountRef.current,
|
|
32
|
+
selectedChannelId: selectedChannel?.id,
|
|
33
|
+
filters,
|
|
34
|
+
})
|
|
35
|
+
}
|
|
51
36
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
})
|
|
65
|
-
}
|
|
37
|
+
// Memoize Preview component to prevent re-renders
|
|
38
|
+
const PreviewComponent = React.useMemo(() => {
|
|
39
|
+
const Preview = (props: ChannelPreviewUIComponentProps) => (
|
|
40
|
+
<CustomChannelPreview
|
|
41
|
+
{...props}
|
|
42
|
+
selectedChannel={selectedChannel}
|
|
43
|
+
onChannelSelect={onChannelSelect}
|
|
44
|
+
debug={debug}
|
|
45
|
+
/>
|
|
46
|
+
)
|
|
47
|
+
return Preview
|
|
48
|
+
}, [selectedChannel, onChannelSelect, debug])
|
|
66
49
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
50
|
+
return (
|
|
51
|
+
<div
|
|
52
|
+
className={classNames(
|
|
53
|
+
'messaging-channel-list h-full flex flex-col min-w-0 overflow-hidden',
|
|
54
|
+
className
|
|
55
|
+
)}
|
|
56
|
+
>
|
|
57
|
+
{/* Channel List */}
|
|
58
|
+
<div className="flex-1 overflow-hidden min-w-0">
|
|
59
|
+
<StreamChannelList
|
|
60
|
+
key={JSON.stringify(filters)}
|
|
61
|
+
filters={filters}
|
|
62
|
+
sort={{ last_message_at: -1 }}
|
|
63
|
+
options={{ limit: 30 }}
|
|
64
|
+
Preview={PreviewComponent}
|
|
65
|
+
EmptyStateIndicator={customEmptyStateIndicator}
|
|
66
|
+
/>
|
|
67
|
+
</div>
|
|
80
68
|
</div>
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
)
|
|
72
|
+
ChannelList.displayName = 'ChannelList'
|
|
@@ -424,14 +424,10 @@ const ChannelViewInner: React.FC<{
|
|
|
424
424
|
renderMessageInputActions,
|
|
425
425
|
onLeaveConversation,
|
|
426
426
|
onBlockParticipant,
|
|
427
|
-
CustomChannelEmptyState = ChannelEmptyState,
|
|
428
427
|
}) => {
|
|
429
428
|
const { channel } = useChannelStateContext()
|
|
430
429
|
const [showInfo, setShowInfo] = useState(false)
|
|
431
430
|
|
|
432
|
-
// Check if channel has messages - using context to reactively subscribe to message updates
|
|
433
|
-
const hasMessages = (channel?.state?.messages?.length ?? 0) > 0
|
|
434
|
-
|
|
435
431
|
// Get participant info for info dialog
|
|
436
432
|
const participant = React.useMemo(() => {
|
|
437
433
|
const members = Object.values(channel.state.members || {})
|
|
@@ -481,13 +477,6 @@ const ChannelViewInner: React.FC<{
|
|
|
481
477
|
hideNewMessageSeparator={false}
|
|
482
478
|
messageActions={[]}
|
|
483
479
|
/>
|
|
484
|
-
|
|
485
|
-
{/* Show custom empty state when no messages */}
|
|
486
|
-
{!hasMessages && CustomChannelEmptyState && (
|
|
487
|
-
<div className="absolute inset-0 w-full h-full">
|
|
488
|
-
<CustomChannelEmptyState />
|
|
489
|
-
</div>
|
|
490
|
-
)}
|
|
491
480
|
</div>
|
|
492
481
|
|
|
493
482
|
{/* Message Input */}
|
|
@@ -513,33 +502,40 @@ const ChannelViewInner: React.FC<{
|
|
|
513
502
|
/**
|
|
514
503
|
* Channel view component with message list and input
|
|
515
504
|
*/
|
|
516
|
-
export const ChannelView
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
<
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
}
|
|
505
|
+
export const ChannelView = React.memo<ChannelViewProps>(
|
|
506
|
+
({
|
|
507
|
+
channel,
|
|
508
|
+
onBack,
|
|
509
|
+
showBackButton = false,
|
|
510
|
+
renderMessageInputActions,
|
|
511
|
+
onLeaveConversation,
|
|
512
|
+
onBlockParticipant,
|
|
513
|
+
className,
|
|
514
|
+
CustomChannelEmptyState = ChannelEmptyState,
|
|
515
|
+
}) => {
|
|
516
|
+
return (
|
|
517
|
+
<div
|
|
518
|
+
className={classNames(
|
|
519
|
+
'messaging-channel-view h-full flex flex-col',
|
|
520
|
+
className
|
|
521
|
+
)}
|
|
522
|
+
>
|
|
523
|
+
<Channel
|
|
524
|
+
channel={channel}
|
|
525
|
+
MessageSystem={CustomSystemMessage}
|
|
526
|
+
EmptyStateIndicator={CustomChannelEmptyState}
|
|
527
|
+
>
|
|
528
|
+
<ChannelViewInner
|
|
529
|
+
onBack={onBack}
|
|
530
|
+
showBackButton={showBackButton}
|
|
531
|
+
renderMessageInputActions={renderMessageInputActions}
|
|
532
|
+
onLeaveConversation={onLeaveConversation}
|
|
533
|
+
onBlockParticipant={onBlockParticipant}
|
|
534
|
+
CustomChannelEmptyState={CustomChannelEmptyState}
|
|
535
|
+
/>
|
|
536
|
+
</Channel>
|
|
537
|
+
</div>
|
|
538
|
+
)
|
|
539
|
+
}
|
|
540
|
+
)
|
|
541
|
+
ChannelView.displayName = 'ChannelView'
|
|
@@ -3,15 +3,4 @@ import React from 'react'
|
|
|
3
3
|
/**
|
|
4
4
|
* Empty state component shown when a channel has no messages
|
|
5
5
|
*/
|
|
6
|
-
export const ChannelEmptyState: React.FC = () =>
|
|
7
|
-
<div className="messaging-channel-empty-state flex items-center justify-center h-full p-8 text-balance">
|
|
8
|
-
<div className="text-center max-w-sm">
|
|
9
|
-
<h2 className="font-semibold text-charcoal mb-2">No messages yet 👀</h2>
|
|
10
|
-
|
|
11
|
-
<p className="text-stone text-xs">
|
|
12
|
-
Share to social media to generate more conversations
|
|
13
|
-
</p>
|
|
14
|
-
</div>
|
|
15
|
-
</div>
|
|
16
|
-
)
|
|
17
|
-
|
|
6
|
+
export const ChannelEmptyState: React.FC = () => null
|
|
@@ -96,9 +96,8 @@ const ChatBubblesIllustration = ({ className }: { className?: string }) => (
|
|
|
96
96
|
/**
|
|
97
97
|
* Empty state component shown when no channel is selected
|
|
98
98
|
*/
|
|
99
|
-
export const EmptyState
|
|
100
|
-
hasChannels
|
|
101
|
-
}> = ({ hasChannels }) => (
|
|
99
|
+
export const EmptyState = React.memo<{ hasChannels: boolean }>(
|
|
100
|
+
({ hasChannels }) => (
|
|
102
101
|
<div className="messaging-empty-state flex items-center justify-center h-full p-8 text-balance">
|
|
103
102
|
<div className="flex flex-col items-center max-w-sm text-center">
|
|
104
103
|
<ChatBubblesIllustration />
|
|
@@ -114,4 +113,5 @@ export const EmptyState: React.FC<{
|
|
|
114
113
|
)}
|
|
115
114
|
</div>
|
|
116
115
|
</div>
|
|
117
|
-
)
|
|
116
|
+
))
|
|
117
|
+
EmptyState.displayName = 'EmptyState'
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
|
|
3
|
+
type ErrorStateProps = {
|
|
4
|
+
message: string
|
|
5
|
+
onBack?: () => void
|
|
6
|
+
}
|
|
7
|
+
|
|
3
8
|
/**
|
|
4
9
|
* Error state component shown when something goes wrong
|
|
5
10
|
*/
|
|
6
|
-
export const ErrorState
|
|
7
|
-
message: string
|
|
8
|
-
onBack?: () => void
|
|
9
|
-
}> = ({ message, onBack }) => (
|
|
11
|
+
export const ErrorState = React.memo<ErrorStateProps>(({ message, onBack }) => (
|
|
10
12
|
<div className="messaging-error-state flex items-center justify-center h-full p-8">
|
|
11
13
|
<div className="text-center max-w-sm">
|
|
12
14
|
<div className="w-24 h-24 bg-danger-alt/20 rounded-full flex items-center justify-center mx-auto mb-6">
|
|
@@ -28,4 +30,5 @@ export const ErrorState: React.FC<{
|
|
|
28
30
|
)}
|
|
29
31
|
</div>
|
|
30
32
|
</div>
|
|
31
|
-
)
|
|
33
|
+
))
|
|
34
|
+
ErrorState.displayName = 'ErrorState'
|
|
@@ -1,13 +1,16 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
1
3
|
import Loading from '../Loading'
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* Loading state component
|
|
5
7
|
*/
|
|
6
|
-
export const LoadingState = () => (
|
|
8
|
+
export const LoadingState = React.memo(() => (
|
|
7
9
|
<div className="messaging-loading-state flex items-center justify-center h-full">
|
|
8
10
|
<div className="flex items-center">
|
|
9
11
|
<Loading className="w-6 h-6" />
|
|
10
12
|
<span className="text-sm text-stone">Loading messages</span>
|
|
11
13
|
</div>
|
|
12
14
|
</div>
|
|
13
|
-
)
|
|
15
|
+
))
|
|
16
|
+
LoadingState.displayName = 'LoadingState'
|
|
@@ -111,7 +111,16 @@ export const MessagingShell: React.FC<MessagingShellProps> = ({
|
|
|
111
111
|
})
|
|
112
112
|
})
|
|
113
113
|
|
|
114
|
-
|
|
114
|
+
// Only update if the set contents have changed to prevent re-renders
|
|
115
|
+
setExistingParticipantIds((prev) => {
|
|
116
|
+
if (
|
|
117
|
+
prev.size === memberIds.size &&
|
|
118
|
+
[...prev].every((id) => memberIds.has(id))
|
|
119
|
+
) {
|
|
120
|
+
return prev
|
|
121
|
+
}
|
|
122
|
+
return memberIds
|
|
123
|
+
})
|
|
115
124
|
setHasChannels(channels.length > 0)
|
|
116
125
|
syncedRef.current = userId // Mark as synced for this user
|
|
117
126
|
|
|
@@ -315,6 +324,15 @@ export const MessagingShell: React.FC<MessagingShellProps> = ({
|
|
|
315
324
|
participantPickerRef.current?.close()
|
|
316
325
|
}, [])
|
|
317
326
|
|
|
327
|
+
const handleDialogBackdropClick = useCallback(
|
|
328
|
+
(e: React.MouseEvent<HTMLDialogElement>) => {
|
|
329
|
+
if (e.target === participantPickerRef.current) {
|
|
330
|
+
handleCloseParticipantPicker()
|
|
331
|
+
}
|
|
332
|
+
},
|
|
333
|
+
[handleCloseParticipantPicker]
|
|
334
|
+
)
|
|
335
|
+
|
|
318
336
|
const handleLeaveConversation = useCallback(
|
|
319
337
|
async (channel: Channel) => {
|
|
320
338
|
if (debug) {
|
|
@@ -459,11 +477,7 @@ export const MessagingShell: React.FC<MessagingShellProps> = ({
|
|
|
459
477
|
<dialog
|
|
460
478
|
ref={participantPickerRef}
|
|
461
479
|
className="mes-dialog"
|
|
462
|
-
onClick={
|
|
463
|
-
if (e.target === participantPickerRef.current) {
|
|
464
|
-
handleCloseParticipantPicker()
|
|
465
|
-
}
|
|
466
|
-
}}
|
|
480
|
+
onClick={handleDialogBackdropClick}
|
|
467
481
|
onClose={handleCloseParticipantPicker}
|
|
468
482
|
>
|
|
469
483
|
<div className="h-full w-full bg-white shadow-max-elevation-light">
|