@messenger-box/platform-mobile 10.0.3-alpha.22 → 10.0.3-alpha.23

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 (29) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/lib/screens/inbox/DialogThreads.js +52 -12
  3. package/lib/screens/inbox/DialogThreads.js.map +1 -1
  4. package/lib/screens/inbox/components/ThreadsViewItem.js +66 -44
  5. package/lib/screens/inbox/components/ThreadsViewItem.js.map +1 -1
  6. package/lib/screens/inbox/containers/ConversationView.js +36 -34
  7. package/lib/screens/inbox/containers/ConversationView.js.map +1 -1
  8. package/lib/screens/inbox/containers/Dialogs.js +82 -37
  9. package/lib/screens/inbox/containers/Dialogs.js.map +1 -1
  10. package/lib/screens/inbox/containers/ThreadConversationView.js +282 -219
  11. package/lib/screens/inbox/containers/ThreadConversationView.js.map +1 -1
  12. package/lib/screens/inbox/containers/ThreadsView.js +83 -50
  13. package/lib/screens/inbox/containers/ThreadsView.js.map +1 -1
  14. package/lib/screens/inbox/hooks/useSafeDialogThreadsMachine.js +108 -0
  15. package/lib/screens/inbox/hooks/useSafeDialogThreadsMachine.js.map +1 -0
  16. package/lib/screens/inbox/workflow/dialog-threads-xstate.js +151 -0
  17. package/lib/screens/inbox/workflow/dialog-threads-xstate.js.map +1 -0
  18. package/package.json +2 -2
  19. package/src/screens/inbox/DialogThreads.tsx +49 -53
  20. package/src/screens/inbox/components/SmartLoader.tsx +61 -0
  21. package/src/screens/inbox/components/ThreadsViewItem.tsx +177 -265
  22. package/src/screens/inbox/containers/ConversationView.tsx +32 -30
  23. package/src/screens/inbox/containers/Dialogs.tsx +57 -22
  24. package/src/screens/inbox/containers/ThreadConversationView.tsx +467 -484
  25. package/src/screens/inbox/containers/ThreadsView.tsx +102 -183
  26. package/src/screens/inbox/hooks/useSafeDialogThreadsMachine.ts +136 -0
  27. package/src/screens/inbox/index.ts +37 -0
  28. package/src/screens/inbox/machines/threadsMachine.ts +147 -0
  29. package/src/screens/inbox/workflow/dialog-threads-xstate.ts +163 -0
@@ -2,51 +2,47 @@ import React, { useMemo } from 'react';
2
2
  import {
3
3
  VStack,
4
4
  Text,
5
- Image,
6
5
  Pressable,
7
6
  HStack,
8
7
  Box,
9
- AvatarGroup,
10
8
  Avatar,
11
9
  AvatarFallbackText,
12
10
  AvatarImage,
13
- AvatarBadge,
14
- View,
15
11
  Button,
16
12
  ButtonText,
13
+ Icon,
14
+ Badge,
15
+ BadgeText,
16
+ Link,
17
+ LinkText,
17
18
  } from '@admin-layout/gluestack-ui-mobile';
18
19
  import { format, isToday, isYesterday } from 'date-fns';
19
20
  import { useFocusEffect } from '@react-navigation/native';
20
21
  import { IChannel, IUserAccount } from 'common';
21
- import {
22
- useThreadMessagesQuery,
23
- useMessagesQuery,
24
- useUserAccountQuery,
25
- useOnThreadChatMessageAddedSubscription,
26
- useOnThreadCreatedUpdatedSubscription,
27
- } from 'common/graphql';
28
22
  import { startCase } from 'lodash-es';
29
23
  import colors from 'tailwindcss/colors';
24
+ import { useSelector } from 'react-redux';
25
+ import { userSelector } from '@adminide-stack/user-auth0-client';
30
26
 
31
- const createdAtText = (value: string) => {
27
+ const timeFormat = (value: string) => {
32
28
  if (!value) return '';
33
29
  let date = new Date(value);
34
- if (isToday(date)) return 'Today';
35
- if (isYesterday(date)) return 'Yesterday';
36
- return format(new Date(value), 'MMM dd, yyyy');
30
+ if (isToday(date)) return format(date, 'h:mm a').toUpperCase();
31
+ return format(date, 'MMM do').toUpperCase();
37
32
  };
38
33
 
39
34
  export interface IDialogListChannel extends IChannel {
40
35
  users: IUserAccount[];
41
36
  }
42
37
 
43
- export interface IDialogListItemProps {
44
- currentUser?: any;
45
- users?: any;
46
- thread?: any;
47
- onOpen: (id: any, title: any, postParentId?: any) => void;
48
- role?: any;
49
- setData?: any;
38
+ export interface ThreadViewItemProps {
39
+ id: string;
40
+ post: any;
41
+ channel: any;
42
+ replies: any[];
43
+ onPress: (id: any, title: any, postParentId?: any) => void;
44
+ channelId?: string;
45
+ channelsDetail?: any;
50
46
  }
51
47
 
52
48
  /**
@@ -54,267 +50,183 @@ export interface IDialogListItemProps {
54
50
  * - Get Reservation info: reservation date, status
55
51
  * - Add ability to get property information: name, logo
56
52
  */
57
- export const ThreadViewItemComponent: React.FC<IDialogListItemProps> = function DialogsListItem({
58
- currentUser,
59
- // users,
60
- thread,
61
- onOpen,
62
- role,
63
- setData,
53
+ export const ThreadViewItemComponent: React.FC<ThreadViewItemProps> = function ThreadViewItem({
54
+ id,
55
+ post,
56
+ channel,
57
+ replies,
58
+ onPress,
59
+ channelId,
60
+ channelsDetail,
64
61
  }) {
65
- // const { data: threadCreatedUpdated } = useOnThreadCreatedUpdatedSubscription({
66
- // variables: {
67
- // channelId: thread?.channel?.id?.toString(),
68
- // postParentId: thread?.post?.id?.toString(),
69
- // },
70
- // });
71
-
72
- useFocusEffect(
73
- React.useCallback(() => {
74
- // Do something when the screen is focused
75
- return () => {
76
- // Do something when the screen is unfocused
77
- // Useful for cleanup functions
78
- };
79
- }, []),
80
- );
81
-
82
- const threadPostReply = React.useMemo(() => {
83
- if (!thread?.replies) return null;
84
- return thread?.replies;
85
- }, [thread]);
62
+ const currentUser = useSelector(userSelector);
86
63
 
87
- // React.useEffect(() => {
88
- // if (threadCreatedUpdated?.threadCreatedUpdated?.data) setData(threadCreatedUpdated?.threadCreatedUpdated?.data);
89
- // }, [threadCreatedUpdated?.threadCreatedUpdated]);
64
+ // Prepare thread replies for display
65
+ const threadReplies = useMemo(() => {
66
+ return replies || [];
67
+ }, [replies]);
90
68
 
91
- // const threadPostReply = React.useMemo(() => {
92
- // if (!thread?.replies) return null;
93
- // if (threadCreatedUpdated?.threadCreatedUpdated?.data?.replies?.length) {
94
- // return threadCreatedUpdated?.threadCreatedUpdated?.data?.replies;
95
- // } else return thread?.replies;
96
- // }, [thread, threadCreatedUpdated]);
97
-
98
- // const lastMessage = useMemo(() => {
99
- // if (!threadPost) {
100
- // return null;
101
- // }
102
- // const replies = threadPost?.replies?.filter((p: any) => p?.message !== '') ?? [];
103
- // if (replies?.length) {
104
- // return replies[0];
105
- // // return replies[replies.length - 1];
106
- // } else {
107
- // const post = threadPost?.post ?? null;
108
- // return post ? post?.post : null;
109
- // }
110
- // }, [threadPost]);
69
+ if (!threadReplies || threadReplies.length === 0) {
70
+ return null;
71
+ }
111
72
 
112
- // const participants: any = React.useMemo(() => {
113
- // if (!thread?.participants?.length) return null;
114
- // return thread?.participants?.filter((u: any) => u?.user?.id !== currentUser?.id) ?? [];
115
- // }, [thread]);
73
+ // Get the last reply for the thread preview
74
+ const lastReply = threadReplies[0]; // Most recent reply should be first in the array
75
+ // Get total number of replies
76
+ const totalReplies = threadReplies.length;
116
77
 
117
- // const allParticipantsNames: any = React.useMemo(() => {
118
- // if (!participants?.length) return null;
119
- // return (
120
- // participants
121
- // ?.map((p: any) => {
122
- // return p?.user?.givenName + ' ' + p?.user?.familyName ?? '';
123
- // })
124
- // ?.join(', ') ?? ''
125
- // );
126
- // }, [participants]);
78
+ // Get the first user in the thread as the primary user
79
+ const primaryUser = channel?.users?.[0] || lastReply?.author;
80
+ const channelName = channel?.title || `${primaryUser?.givenName || ''} ${primaryUser?.familyName || ''}`;
127
81
 
128
- if (!threadPostReply || threadPostReply?.length == 0) {
129
- return <></>;
130
- }
82
+ // Determine if the current user is in the thread
83
+ const userIsInThread = channel?.users?.some((user: any) => user.id === currentUser?.id);
131
84
 
132
85
  return (
133
- <VStack
134
- space={'sm'}
135
- className="flex-1 py-2 px-2 border rounded-md bg-white border-gray-200 dark:border-white dark:bg-white"
86
+ <Pressable
87
+ onPress={() => onPress(channel?.id, 'Thread', post?.id)}
88
+ style={{
89
+ marginVertical: 5,
90
+ backgroundColor: 'white',
91
+ borderRadius: 10,
92
+ paddingVertical: 5,
93
+ elevation: 1,
94
+ shadowColor: '#000',
95
+ shadowOffset: { width: 0, height: 1 },
96
+ shadowOpacity: 0.1,
97
+ shadowRadius: 2,
98
+ }}
136
99
  >
137
- <HStack space={'sm'} className="py-2 w-[100%] flex-1 justify-between items-center">
138
- <Box className="flex-1">
139
- {/* <HStack flex={1} space={5} py={2} alignItems={'baseline'}>
140
- <Box>
141
- <Avatar.Badge size={4} left={0} zIndex={1} bg="green.800" />
142
- </Box>
143
- <Box>
144
- <Text color="gray.600" fontSize="lg" flexWrap={'wrap'} fontWeight="semibold">
145
- {allParticipantsNames || ''}
146
- </Text>
147
- </Box>
100
+ <Box className="mb-3">
101
+ <VStack space="md">
102
+ {/* Thread header with green dot and users */}
103
+ {/* <HStack space="sm" className="px-4 items-center">
104
+ <Box
105
+ className="bg-green-500 rounded-full"
106
+ style={{ width: 8, height: 8 }}
107
+ />
108
+ <Text className="text-base text-gray-900">{channelName}</Text>
148
109
  </HStack> */}
149
- <HStack space={'sm'} className="flex-1 justify-center items-center">
150
- <Box className="flex-1">
151
- {/* <HStack space={2} py={2}>
152
- <Box>
110
+
111
+ {/* Thread members */}
112
+ <Text className="text-sm font-medium text-gray-600 px-4 pt-2">
113
+ {channel?.users?.map((user: any) => user?.givenName || user?.username)?.join(', ')}
114
+ </Text>
115
+
116
+ {/* Thread messages preview - show up to 3 most recent messages */}
117
+ {threadReplies.slice(0, 3).map((reply: any, index: number) => {
118
+ // Group consecutive messages from the same user
119
+ const previousReply = index > 0 ? threadReplies.slice(0, 3)[index - 1] : null;
120
+ const isConsecutiveReply = previousReply && previousReply.author?.id === reply.author?.id;
121
+
122
+ return (
123
+ <HStack key={reply.id || index} space="md" className="px-4 py-1">
124
+ {!isConsecutiveReply ? (
153
125
  <Avatar
154
- bg={'transparent'}
155
- size={8}
156
- _image={{ borderRadius: 6, borderWidth: 2, borderColor: '#fff' }}
157
- source={{
158
- uri: thread?.post?.author?.picture,
126
+ size="md"
127
+ style={{
128
+ width: 40,
129
+ height: 40,
130
+ backgroundColor:
131
+ reply?.author?.id === currentUser?.id ? '#2EB67D' : '#E8A54A',
159
132
  }}
160
133
  >
161
- {startCase(thread?.post?.author?.username?.charAt(0))}
134
+ <AvatarFallbackText>
135
+ {startCase(reply?.author?.username?.charAt(0) || 'U')}
136
+ </AvatarFallbackText>
137
+ {reply?.author?.picture && (
138
+ <AvatarImage
139
+ alt={reply?.author?.username || 'User'}
140
+ source={{
141
+ uri: reply?.author?.picture,
142
+ }}
143
+ />
144
+ )}
162
145
  </Avatar>
163
- </Box>
164
- <Box>
165
- <HStack space={4}>
166
- <Text>
167
- {thread?.post?.author?.givenName + ' ' + thread?.post?.author?.familyName ??
168
- ''}
169
- </Text>
170
- <Text color="gray.500">
171
- {thread?.post ? createdAtText(thread?.post?.createdAt) : ''}
172
- </Text>
173
- </HStack>
174
- <Text color="gray.600" noOfLines={2}>
175
- {thread?.post?.message ?? ''}
176
- </Text>
177
- {!thread?.replies?.length && (
178
- <Button
179
- size={'xs'}
180
- w={150}
181
- variant={'outline'}
182
- _text={{ fontSize: 12, color: '#000' }}
183
- onPress={() =>
184
- onOpen(thread?.channel?.id, thread?.channel?.title, thread?.post?.id)
185
- }
186
- >
187
- Reply
188
- </Button>
146
+ ) : (
147
+ <Box style={{ width: 40 }} />
148
+ )}
149
+
150
+ <VStack space="xs" className="flex-1">
151
+ {!isConsecutiveReply && (
152
+ <HStack space="sm" className="items-center">
153
+ <Text className="font-bold text-gray-900">
154
+ {reply?.author?.givenName || reply?.author?.username || 'User'}
155
+ </Text>
156
+ <Text className="text-xs text-gray-500">
157
+ {timeFormat(reply?.createdAt)}
158
+ </Text>
159
+ </HStack>
189
160
  )}
190
- </Box>
191
- </HStack> */}
192
- {threadPostReply?.length > 0 && (
193
- <>
194
- {threadPostReply
195
- ?.slice(0, 2)
196
- ?.reverse()
197
- ?.map((reply: any, index: any) => (
198
- <HStack key={index} space={'sm'} className="pb-2 pt-1">
199
- <Box>
200
- <Avatar
201
- key={'thread-view-key-' + index}
202
- size={'md'}
203
- className="bg-transparent top-0 right-0 z-[1]"
204
- >
205
- <AvatarFallbackText>
206
- {' '}
207
- {startCase(reply?.author?.username?.charAt(0))}
208
- </AvatarFallbackText>
209
- <AvatarImage
210
- alt="image"
211
- style={{
212
- borderRadius: 6,
213
- borderWidth: 2,
214
- borderColor: '#fff',
215
- }}
216
- source={{
217
- uri: reply?.author?.picture,
218
- }}
219
- />
220
- </Avatar>
221
- </Box>
222
- <Box>
223
- <HStack space={'md'}>
224
- <Text>
225
- {reply?.author?.givenName ?? '' ?? ''}{' '}
226
- {reply?.author?.familyName ?? '' ?? ''}
227
- {/* {lastMessage?.author?.givenName +
228
- ' ' +
229
- lastMessage?.author?.familyName ?? ''} */}
230
- </Text>
231
- <Text color={colors.gray[500]}>
232
- {reply?.createdAt ? createdAtText(reply?.createdAt) : ''}
233
- {/* {lastMessage ? createdAtText(lastMessage?.createdAt) : ''} */}
234
- </Text>
235
- </HStack>
236
- <VStack space={'sm'}>
237
- {reply?.message && (
238
- <Text color={colors.gray[600]} numberOfLines={2}>
239
- {reply?.message.length < 70
240
- ? `${reply?.message}`
241
- : `${reply?.message.substring(0, 68)}....`}
242
- </Text>
243
- )}
244
- {/* <Text color="gray.600" noOfLines={2}>
245
- {reply?.message
246
- ? reply?.message.length < 70
247
- ? `${reply?.message}`
248
- : `${reply?.message.substring(0, 68)}....`
249
- : ''}
250
- </Text> */}
251
161
 
252
- <>
253
- {reply?.files?.data?.length > 0 &&
254
- reply?.files?.data?.map((f: any, index: any) => (
255
- <Box>
256
- <Avatar
257
- key={'thread-view-other-key-' + index}
258
- className="bg-transparent"
259
- size={'md'}
260
- >
261
- <AvatarFallbackText> I</AvatarFallbackText>
262
- <AvatarImage
263
- alt="image"
264
- style={{
265
- borderRadius: 6,
266
- borderWidth: 2,
267
- borderColor: '#fff',
268
- }}
269
- source={{
270
- uri: f?.url,
271
- }}
272
- />
273
- </Avatar>
274
- </Box>
275
- ))}
276
- </>
277
- </VStack>
162
+ {reply?.message && (
163
+ <Text color={colors.gray[700]} numberOfLines={2} className="text-base">
164
+ {reply?.message}
165
+ {reply?.edited && <Text className="text-xs text-gray-500"> (edited)</Text>}
166
+ </Text>
167
+ )}
278
168
 
279
- {(threadPostReply?.length == 1 || index !== 0) && (
280
- <Button
281
- size={'xs'}
282
- className="w-[150]"
283
- variant={'outline'}
284
- onPress={() =>
285
- onOpen(
286
- thread?.channel?.id,
287
- thread?.channel?.title,
288
- thread?.post?.id,
289
- )
290
- }
291
- >
292
- <ButtonText style={{ fontSize: 12, color: '#000' }}>
293
- Reply
294
- </ButtonText>
295
- </Button>
296
- )}
169
+ {reply.email && <Text className="text-blue-500">{reply.email}</Text>}
170
+
171
+ {reply?.files?.data?.length > 0 && (
172
+ <HStack space="sm" className="my-1">
173
+ {reply?.files?.data?.map((file: any, fileIndex: number) => (
174
+ <Box
175
+ key={fileIndex}
176
+ className="overflow-hidden"
177
+ style={{ width: 80, height: 80 }}
178
+ >
179
+ <AvatarImage
180
+ alt="attachment"
181
+ className="rounded-none border-none"
182
+ style={{
183
+ width: '100%',
184
+ height: '100%',
185
+ }}
186
+ source={{
187
+ uri: file?.url,
188
+ }}
189
+ />
297
190
  </Box>
298
- </HStack>
299
- ))}
300
- </>
301
- )}
302
- </Box>
303
- </HStack>
304
- </Box>
305
- </HStack>
306
- {/* <HStack>
307
- <Box flex={0.06} alignItems={'flex-start'}></Box>
308
- <Button
309
- size={'xs'}
310
- variant={'outline'}
311
- _text={{ fontSize: 12, color: '#000' }}
312
- onPress={() => onOpen(thread?.channel?.id, thread?.channel?.title, postParentId)}
313
- >
314
- Reply
315
- </Button>
316
- </HStack> */}
317
- </VStack>
191
+ ))}
192
+ </HStack>
193
+ )}
194
+ </VStack>
195
+ </HStack>
196
+ );
197
+ })}
198
+
199
+ {/* Show more replies indicator */}
200
+ {totalReplies > 3 && (
201
+ <HStack className="px-4 items-center" space="sm">
202
+ <Box style={{ width: 40 }} />
203
+ <Link onPress={() => onPress(channel?.id, 'Thread', post?.id)}>
204
+ <LinkText className="text-blue-600 mt-1">
205
+ {totalReplies - 3} more {totalReplies - 3 === 1 ? 'reply' : 'replies'}
206
+ </LinkText>
207
+ </Link>
208
+ </HStack>
209
+ )}
210
+
211
+ {/* Reply button */}
212
+ <Box className="px-4 pb-2">
213
+ <Button
214
+ size="sm"
215
+ className="self-start rounded-full"
216
+ variant="outline"
217
+ style={{
218
+ borderColor: '#E2E8F0',
219
+ paddingHorizontal: 16,
220
+ paddingVertical: 6,
221
+ }}
222
+ onPress={() => onPress(channel?.id, 'Thread', post?.id)}
223
+ >
224
+ <ButtonText style={{ fontSize: 14, color: colors.gray[800] }}>Reply</ButtonText>
225
+ </Button>
226
+ </Box>
227
+ </VStack>
228
+ </Box>
229
+ </Pressable>
318
230
  );
319
231
  };
320
232
 
@@ -1066,19 +1066,6 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
1066
1066
  return [];
1067
1067
  }
1068
1068
 
1069
- // Log the first message for debugging
1070
- if (state.context.channelMessages[0]) {
1071
- const sampleMsg = state.context.channelMessages[0];
1072
- console.log(
1073
- '📷 Sample message files:',
1074
- JSON.stringify({
1075
- hasFiles: !!sampleMsg.files,
1076
- fileCount: sampleMsg.files?.data?.length || 0,
1077
- fileUrl: sampleMsg.files?.data?.[0]?.url || 'none',
1078
- }),
1079
- );
1080
- }
1081
-
1082
1069
  // Use a more efficient approach - pre-filter messages once
1083
1070
  const filteredMessages = uniqBy(state.context.channelMessages, ({ id }) => id);
1084
1071
 
@@ -1097,7 +1084,6 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
1097
1084
  const fileData = msg.files.data[0];
1098
1085
  if (fileData && fileData.url) {
1099
1086
  imageUrl = fileData.url;
1100
- console.log('📷 Found image URL for message', msg.id, ':', imageUrl);
1101
1087
  }
1102
1088
  }
1103
1089
 
@@ -1120,7 +1106,7 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
1120
1106
  isShowThreadMessage,
1121
1107
  };
1122
1108
  });
1123
- }, [state?.context?.channelMessages, state?.context?.channelId, isShowThreadMessage]);
1109
+ }, [state?.context?.channelMessages, isShowThreadMessage]);
1124
1110
 
1125
1111
  // Memoize the renderMessageText function
1126
1112
  const renderMessageText = useCallback(
@@ -1592,8 +1578,8 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
1592
1578
 
1593
1579
  lastProcessedMessageRef.current = newMessage.id;
1594
1580
 
1595
- // Send update to state machine using a timeout to break the render cycle
1596
- setTimeout(() => {
1581
+ // Use a batch update strategy to avoid frequent re-renders
1582
+ queueMicrotask(() => {
1597
1583
  safeSend({
1598
1584
  type: ConversationActions.SET_CHANNEL_MESSAGES,
1599
1585
  data: {
@@ -1604,7 +1590,7 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
1604
1590
  totalCount: (prev?.messages?.totalCount || 0) + 1,
1605
1591
  },
1606
1592
  });
1607
- }, 0);
1593
+ });
1608
1594
 
1609
1595
  return {
1610
1596
  ...prev,
@@ -1632,7 +1618,7 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
1632
1618
  // Add ref to track last processed message
1633
1619
  const lastProcessedMessageRef = useRef(null);
1634
1620
 
1635
- // Add optimized listViewProps to reduce re-renders
1621
+ // Add optimized listViewProps to reduce re-renders and improve list performance
1636
1622
  const listViewProps = useMemo(
1637
1623
  () => ({
1638
1624
  onEndReached: onEndReached,
@@ -1640,8 +1626,10 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
1640
1626
  onMomentumScrollBegin: onMomentumScrollBegin,
1641
1627
  removeClippedSubviews: true, // Improve performance by unmounting components when not visible
1642
1628
  initialNumToRender: 10, // Reduce initial render amount
1643
- maxToRenderPerBatch: 10, // Reduce number in each render batch
1644
- windowSize: 10, // Reduce the window size
1629
+ maxToRenderPerBatch: 7, // Reduce number in each render batch
1630
+ windowSize: 7, // Reduce the window size
1631
+ updateCellsBatchingPeriod: 50, // Batch cell updates to improve scrolling
1632
+ keyExtractor: (item) => item._id, // Add explicit key extractor
1645
1633
  }),
1646
1634
  [onEndReached, onMomentumScrollBegin],
1647
1635
  );
@@ -1712,13 +1700,9 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
1712
1700
  safeSend({ type: ConversationActions.SET_MESSAGE_TEXT, data: { messageText: text } })
1713
1701
  }
1714
1702
  renderFooter={() =>
1715
- safeContextProperty('loading') ? (
1716
- <Spinner color={'#3b82f6'} />
1717
- ) : safeContextProperty('imageLoading') ? (
1703
+ safeContextProperty('loading') || safeContextProperty('imageLoading') ? (
1718
1704
  <Spinner color={'#3b82f6'} />
1719
- ) : (
1720
- ''
1721
- )
1705
+ ) : null
1722
1706
  }
1723
1707
  scrollToBottom
1724
1708
  user={{
@@ -1775,8 +1759,26 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
1775
1759
  };
1776
1760
 
1777
1761
  const SubscriptionHandler = ({ subscribeToNewMessages, channelId }: ISubscriptionHandlerProps) => {
1778
- useEffect(() => subscribeToNewMessages(), [channelId]);
1779
- return <></>;
1762
+ // Use memo for the channelId dependency to prevent unnecessary subscriptions
1763
+ const channelIdRef = useRef(channelId);
1764
+
1765
+ useEffect(() => {
1766
+ if (channelId && channelId !== channelIdRef.current) {
1767
+ channelIdRef.current = channelId;
1768
+ console.log('Setting up subscription for channel:', channelId);
1769
+ return subscribeToNewMessages();
1770
+ }
1771
+ }, [channelId, subscribeToNewMessages]);
1772
+
1773
+ return null;
1780
1774
  };
1781
1775
 
1782
- export const ConversationView = React.memo(ConversationViewComponent);
1776
+ // Export with React.memo to prevent unnecessary re-renders
1777
+ export const ConversationView = React.memo(ConversationViewComponent, (prevProps, nextProps) => {
1778
+ // Only re-render if these critical props change
1779
+ return (
1780
+ prevProps.channelId === nextProps.channelId &&
1781
+ prevProps.role === nextProps.role &&
1782
+ prevProps.isShowThreadMessage === nextProps.isShowThreadMessage
1783
+ );
1784
+ });