@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
@@ -307,7 +307,6 @@ function useSafeMachine(machine) {
307
307
  `Merging ${newMessages.length} older messages with ${prev.context.threadMessages.length} existing messages`,
308
308
  );
309
309
 
310
- // Debug: Log the dates of the messages for troubleshooting
311
310
  if (newMessages.length > 0) {
312
311
  try {
313
312
  console.log(
@@ -677,7 +676,14 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
677
676
  // Fetch thread messages function
678
677
  const fetchThreadMessages = useCallback(() => {
679
678
  if (channelId && parentId) {
680
- console.log('Initial fetch of thread messages using larger limit (50)');
679
+ if (__DEV__) console.log('Initial fetch of thread messages using larger limit (50)');
680
+
681
+ // Set loading state
682
+ safeSend({
683
+ type: ThreadActions.START_LOADING,
684
+ data: { loading: true },
685
+ });
686
+
681
687
  getThreadMessages({
682
688
  variables: {
683
689
  channelId: channelId?.toString(),
@@ -695,10 +701,12 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
695
701
  const messageTotalCount = threads?.replyCount ?? 0;
696
702
  const messages = [...threadReplies];
697
703
 
698
- console.log(
699
- `Initial fetch complete. Got ${messages.length} messages of ${messageTotalCount} total`,
700
- );
704
+ if (__DEV__)
705
+ console.log(
706
+ `Initial fetch complete. Got ${messages.length} messages of ${messageTotalCount} total`,
707
+ );
701
708
 
709
+ // Use batch updates to reduce render cycles
702
710
  safeSend({
703
711
  type: ThreadActions.SET_THREAD_MESSAGES,
704
712
  data: {
@@ -717,7 +725,7 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
717
725
  });
718
726
  });
719
727
  }
720
- }, [channelId, parentId, role]);
728
+ }, [channelId, parentId, role, getThreadMessages, safeSend]);
721
729
 
722
730
  React.useEffect(() => {
723
731
  if (data?.getPostThread) {
@@ -819,193 +827,41 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
819
827
  }
820
828
  }, [threadMessageListRef]);
821
829
 
822
- const onFetchOld = useCallback(() => {
823
- const totalCount = safeContextProperty('totalCount', 0);
824
- const threadMessages = safeContextProperty('threadMessages', []);
825
-
826
- if (totalCount > threadMessages.length && !safeContextProperty('loadingOldMessages', false)) {
827
- console.log('Loading more messages - current count:', threadMessages.length, 'of', totalCount);
828
-
829
- // Set the loading state specifically for old messages
830
- safeSend({
831
- type: ThreadActions.FETCH_MORE_MESSAGES,
832
- data: { loadingOldMessages: true },
833
- });
834
-
835
- // Since Skip=0, Limit=50 worked well, let's use that approach
836
- console.log('Using proven approach: Skip=0, Limit=50');
837
-
838
- // Try with a larger limit and skip=0, which we know works
839
- const queryVariables = {
840
- channelId: channelId?.toString(),
841
- role: role?.toString(),
842
- postParentId: !parentId || parentId == 0 ? null : parentId?.toString(),
843
- selectedFields: 'id channel post replies replyCount lastReplyAt createdAt updatedAt',
844
- limit: 50, // Use larger limit that proved to work
845
- skip: 0, // Start from the beginning
846
- };
847
-
848
- console.log('Query variables:', JSON.stringify(queryVariables));
849
-
850
- fetchMoreMessages({
851
- variables: queryVariables,
852
- })
853
- .then((res: any) => {
854
- console.log('API response received:', JSON.stringify(res?.data, null, 2));
855
-
856
- if (res?.data?.getPostThread) {
857
- const threads: any = res?.data?.getPostThread;
858
- const threadReplies = threads?.replies ?? [];
859
- // Get the actual total count from the response
860
- const actualTotalCount = threads?.replyCount ?? 0;
861
-
862
- console.log('API response details:');
863
- console.log('- replyCount:', threads?.replyCount);
864
- console.log('- replies array length:', threadReplies.length);
865
- console.log(
866
- '- replies structure:',
867
- threadReplies.length > 0
868
- ? `First item has fields: ${Object.keys(threadReplies[0]).join(', ')}`
869
- : 'No items',
870
- );
871
-
872
- if (threadReplies.length > 0) {
873
- console.log('- Sample message:', JSON.stringify(threadReplies[0], null, 2));
874
- }
875
-
876
- console.log(
877
- 'Successfully loaded more messages:',
878
- threadReplies.length,
879
- 'of total:',
880
- actualTotalCount,
881
- );
882
- console.log('Thread reply IDs:', threadReplies.map((msg) => msg.id).join(', '));
883
-
884
- // Compare with our existing messages to find the ones we're missing
885
- const existingIds = new Set(threadMessages.map((msg) => msg.id));
886
- const newUniqueMessages = threadReplies.filter((msg) => !existingIds.has(msg.id));
887
-
888
- console.log(
889
- `Found ${newUniqueMessages.length} unique new messages out of ${threadReplies.length} received`,
890
- );
891
-
892
- // If we've received all messages but our count doesn't match, update the total count
893
- if (actualTotalCount !== totalCount) {
894
- console.log(
895
- `Updating totalCount from ${totalCount} to ${actualTotalCount} based on API response`,
896
- );
897
- // This will be applied below when we process the messages
898
- }
899
-
900
- // If no new unique messages, it means we already have everything
901
- if (newUniqueMessages.length === 0) {
902
- console.log('No new unique messages found, adjusting total count');
903
-
904
- // Register this as a failed load attempt
905
- registerLoadAttemptResult(false);
906
-
907
- // Adjust total count to match what we have
908
- safeSend({
909
- type: ThreadActions.SET_THREAD_MESSAGES,
910
- data: {
911
- messages: threadMessages,
912
- totalCount: threadMessages.length,
913
- threadPost: safeContextProperty('threadPost', []),
914
- postThread: safeContextProperty('postThread', null),
915
- },
916
- });
917
- return;
918
- }
919
-
920
- // Register success since we found new messages
921
- registerLoadAttemptResult(true);
922
-
923
- console.log(`Adding ${newUniqueMessages.length} new messages to thread`);
924
-
925
- safeSend({
926
- type: 'FETCH_MORE_MESSAGES_SUCCESS',
927
- data: {
928
- messages: newUniqueMessages,
929
- totalCount: actualTotalCount,
930
- loadingOldMessages: false,
931
- },
932
- });
933
- } else {
934
- console.log('No thread data returned when loading more messages');
935
- registerLoadAttemptResult(false);
936
- safeSend({
937
- type: ThreadActions.STOP_LOADING,
938
- data: { loadingOldMessages: false },
939
- });
940
- }
941
- })
942
- .catch((error: any) => {
943
- console.error('Error fetching more messages:', error);
944
- registerLoadAttemptResult(false);
945
- safeSend({
946
- type: 'ERROR',
947
- data: {
948
- message: error.message,
949
- loadingOldMessages: false,
950
- },
951
- });
952
- });
953
- } else {
954
- console.log('No more messages to load or already loading');
955
- // Make sure we're not stuck in loading state
956
- if (safeContextProperty('loadingOldMessages', false)) {
957
- safeSend({
958
- type: ThreadActions.STOP_LOADING,
959
- data: { loadingOldMessages: false },
960
- });
961
- }
962
- }
963
- }, [parentId, channelId, state.context, registerLoadAttemptResult]);
964
-
965
- let onScroll = false;
830
+ const isCloseToTop = useCallback(({ layoutMeasurement, contentOffset, contentSize }) => {
831
+ // We consider being "close to top" when scrolled to top 15% of visible area
832
+ const visibleHeight = layoutMeasurement.height;
833
+ const topThreshold = Math.min(80, visibleHeight * 0.15);
834
+ return contentOffset.y <= topThreshold;
835
+ }, []);
966
836
 
967
- const handleScrollToTop = ({ nativeEvent }: any) => {
968
- // Check if we're near the top of the list
969
- if (isCloseToTop(nativeEvent)) {
970
- const isLoading = safeContextProperty('loadingOldMessages', false);
971
- const totalCount = safeContextProperty('totalCount', 0);
972
- const currentCount = safeContextProperty('threadMessages', []).length;
973
- const hasMoreMessages = totalCount > currentCount;
837
+ const handleScrollToTop = useCallback(
838
+ ({ nativeEvent }: any) => {
839
+ // Check if we're near the top of the list
840
+ if (isCloseToTop(nativeEvent)) {
841
+ const isLoading = safeContextProperty('loadingOldMessages', false);
842
+ const totalCount = safeContextProperty('totalCount', 0);
843
+ const currentCount = safeContextProperty('threadMessages', []).length;
844
+ const hasMoreMessages = totalCount > currentCount;
974
845
 
975
- console.log(
976
- `Scroll near top - Loading state: ${isLoading}, Messages: ${currentCount}/${totalCount}, Has more: ${hasMoreMessages}`,
977
- );
846
+ if (__DEV__)
847
+ console.log(
848
+ `Scroll near top - Loading state: ${isLoading}, Messages: ${currentCount}/${totalCount}, Has more: ${hasMoreMessages}`,
849
+ );
978
850
 
979
- if (!isLoading && hasMoreMessages) {
980
- console.log('Near top of list - loading older messages');
981
- safeSend({ type: ThreadActions.FETCH_MORE_MESSAGES });
851
+ if (!isLoading && hasMoreMessages) {
852
+ if (__DEV__) console.log('Near top of list - loading older messages');
853
+ safeSend({ type: ThreadActions.FETCH_MORE_MESSAGES });
854
+ }
982
855
  }
983
- }
984
- };
856
+ },
857
+ [isCloseToTop, safeContextProperty, safeSend],
858
+ );
985
859
 
986
860
  const handleEndReached = () => {
987
861
  // This triggers when scrolled to the bottom
988
862
  console.log('Reached end of message list');
989
863
  };
990
864
 
991
- const isCloseToTop = ({ layoutMeasurement, contentOffset, contentSize }) => {
992
- // We consider being "close to top" when scrolled to top 15% of visible area
993
- const visibleHeight = layoutMeasurement.height;
994
- const topThreshold = Math.min(80, visibleHeight * 0.15);
995
-
996
- // For debugging
997
- const distanceFromTop = contentOffset.y;
998
- const totalContentHeight = contentSize.height;
999
-
1000
- console.log(
1001
- `Scroll position: ${distanceFromTop.toFixed(0)}px from top, threshold: ${topThreshold.toFixed(
1002
- 0,
1003
- )}px, content height: ${totalContentHeight.toFixed(0)}px`,
1004
- );
1005
-
1006
- return contentOffset.y <= topThreshold;
1007
- };
1008
-
1009
865
  const onSelectImages = async () => {
1010
866
  try {
1011
867
  safeSend({ type: ThreadActions.START_LOADING });
@@ -1299,236 +1155,112 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
1299
1155
  }
1300
1156
  };
1301
1157
 
1158
+ // Optimize messageList memo to avoid unnecessary recalculations
1302
1159
  const messageList = useMemo(() => {
1303
1160
  const threadMessages = safeContextProperty('threadMessages', []);
1304
- console.log(`Creating message list from ${threadMessages.length} thread messages`);
1305
-
1306
- let res: any = [];
1307
- if (threadMessages?.length) {
1308
- // We need to convert the threadMessages into the format expected by GiftedChat
1309
- // Use a Set to track IDs and prevent duplicates
1310
- const messageIds = new Set();
1311
-
1312
- res = threadMessages
1313
- .filter((msg) => {
1314
- // Skip duplicate IDs
1315
- if (!msg.id || messageIds.has(msg.id)) {
1316
- console.log('Skipping duplicate message ID:', msg.id);
1317
- return false;
1318
- }
1319
- messageIds.add(msg.id);
1320
- return true;
1321
- })
1322
- .map((msg) => {
1323
- // Generate a unique _id if needed by combining id and createdAt
1324
- const uniqueId = msg.id || `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
1161
+ // Avoid excessive logging in production
1162
+ if (__DEV__) console.log(`Creating message list from ${threadMessages.length} thread messages`);
1325
1163
 
1326
- // Safely create a Date object with validation
1327
- let messageDate;
1328
- try {
1329
- // Check if createdAt is a valid date string
1330
- if (msg.createdAt && !isNaN(new Date(msg.createdAt).getTime())) {
1331
- messageDate = new Date(msg.createdAt);
1332
- } else {
1333
- // Use current time as fallback if date is invalid
1334
- console.warn(`Invalid date value for message ${msg.id}: ${msg.createdAt}`);
1335
- messageDate = new Date();
1336
- }
1337
- } catch (error) {
1338
- console.error(`Error creating date for message ${msg.id}:`, error);
1339
- messageDate = new Date(); // Fallback to current time
1164
+ if (!threadMessages?.length) return [];
1165
+
1166
+ // We need to convert the threadMessages into the format expected by GiftedChat
1167
+ // Use a Set to track IDs and prevent duplicates
1168
+ const messageIds = new Set();
1169
+
1170
+ const res = threadMessages
1171
+ .filter((msg) => {
1172
+ // Skip duplicate IDs
1173
+ if (!msg.id || messageIds.has(msg.id)) return false;
1174
+ messageIds.add(msg.id);
1175
+ return true;
1176
+ })
1177
+ .map((msg) => {
1178
+ // Generate a unique _id if needed by combining id and createdAt
1179
+ const uniqueId = msg.id || `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
1180
+
1181
+ // Safely create a Date object with validation
1182
+ let messageDate;
1183
+ try {
1184
+ // Check if createdAt is a valid date string
1185
+ if (msg.createdAt && !isNaN(new Date(msg.createdAt).getTime())) {
1186
+ messageDate = new Date(msg.createdAt);
1187
+ } else {
1188
+ // Use current time as fallback if date is invalid
1189
+ messageDate = new Date();
1340
1190
  }
1191
+ } catch (error) {
1192
+ messageDate = new Date(); // Fallback to current time
1193
+ }
1341
1194
 
1342
- // Extract image URL from files data
1343
- let imageUrl = null;
1344
- if (msg.files?.data && msg.files.data.length > 0) {
1345
- const fileData = msg.files.data[0];
1346
- if (fileData && fileData.url) {
1347
- imageUrl = fileData.url;
1348
- console.log('📷 Found image URL for message', msg.id, ':', imageUrl);
1349
- }
1195
+ // Extract image URL from files data
1196
+ let imageUrl = null;
1197
+ if (msg.files?.data && msg.files.data.length > 0) {
1198
+ const fileData = msg.files.data[0];
1199
+ if (fileData && fileData.url) {
1200
+ imageUrl = fileData.url;
1350
1201
  }
1202
+ }
1351
1203
 
1352
- // Get the message text without adding "File attachment" for image-only messages
1353
- let messageText = msg.message || '';
1354
-
1355
- let message: IMessageProps = {
1356
- _id: uniqueId,
1357
- text: messageText,
1358
- createdAt: messageDate,
1359
- user: {
1360
- _id: msg?.author?.id ?? auth?.profile?.id,
1361
- name:
1362
- msg?.author?.givenName ??
1363
- auth?.profile?.given_name + ' ' + msg?.author?.familyName ??
1364
- auth?.profile?.family_name,
1365
- avatar: msg?.author?.picture ?? auth?.profile?.picture,
1366
- },
1367
- type: msg?.type || '',
1368
- image: imageUrl,
1369
- sent: msg?.isDelivered || true,
1370
- received: msg?.isRead || false,
1371
- propsConfiguration: msg?.propsConfiguration,
1372
- };
1373
- return message;
1374
- });
1375
- }
1204
+ // Get the message text without adding "File attachment" for image-only messages
1205
+ let messageText = msg.message || '';
1206
+
1207
+ return {
1208
+ _id: uniqueId,
1209
+ text: messageText,
1210
+ createdAt: messageDate,
1211
+ user: {
1212
+ _id: msg?.author?.id ?? auth?.profile?.id,
1213
+ name:
1214
+ msg?.author?.givenName ??
1215
+ auth?.profile?.given_name + ' ' + msg?.author?.familyName ??
1216
+ auth?.profile?.family_name,
1217
+ avatar: msg?.author?.picture ?? auth?.profile?.picture,
1218
+ },
1219
+ type: msg?.type || '',
1220
+ image: imageUrl,
1221
+ sent: msg?.isDelivered || true,
1222
+ received: msg?.isRead || false,
1223
+ propsConfiguration: msg?.propsConfiguration,
1224
+ };
1225
+ });
1376
1226
 
1377
1227
  // Sort messages by date (newest first as required by GiftedChat)
1378
1228
  // Use a safer getTime() approach with error handling
1379
- const sortedMessages = orderBy(
1229
+ return orderBy(
1380
1230
  res,
1381
1231
  [
1382
1232
  (msg) => {
1383
1233
  try {
1384
1234
  return msg.createdAt instanceof Date ? msg.createdAt.getTime() : new Date().getTime();
1385
1235
  } catch (error) {
1386
- console.error('Error sorting message by date:', error);
1387
1236
  return 0; // Fallback to a default value
1388
1237
  }
1389
1238
  },
1390
1239
  ],
1391
1240
  ['desc'],
1392
1241
  );
1393
-
1394
- // Log message dates to help with debugging
1395
- if (sortedMessages.length > 0) {
1396
- try {
1397
- const firstMsg = sortedMessages[0];
1398
- const lastMsg = sortedMessages[sortedMessages.length - 1];
1399
-
1400
- console.log(
1401
- 'Message date range:',
1402
- lastMsg.createdAt instanceof Date ? lastMsg.createdAt.toISOString() : 'invalid date',
1403
- 'to',
1404
- firstMsg.createdAt instanceof Date ? firstMsg.createdAt.toISOString() : 'invalid date',
1405
- );
1406
- } catch (error) {
1407
- console.error('Error logging message date range:', error);
1408
- }
1409
- }
1410
-
1411
- return sortedMessages;
1412
1242
  }, [safeContextProperty('threadMessages'), auth]);
1413
1243
 
1414
- const renderSend = (props) => {
1415
- // Check if there's an image selected
1416
- const hasImage = safeContextProperty('selectedImage', '') !== '';
1244
+ // Optimize render functions with memoization
1245
+ const renderSend = useCallback(
1246
+ (props) => {
1247
+ // Check if there's an image selected
1248
+ const hasImage = safeContextProperty('selectedImage', '') !== '';
1417
1249
 
1418
- // Enable send button if there's text OR an image
1419
- const isDisabled = !hasImage && (!props.text || props.text.trim().length === 0);
1250
+ // Enable send button if there's text OR an image
1251
+ const isDisabled = !hasImage && (!props.text || props.text.trim().length === 0);
1420
1252
 
1421
- return (
1422
- <Send
1423
- {...props}
1424
- containerStyle={{
1425
- alignItems: 'center',
1426
- justifyContent: 'center',
1427
- marginHorizontal: 4,
1428
- marginBottom: 0,
1429
- }}
1430
- disabled={isDisabled}
1431
- >
1432
- <Box
1433
- style={{
1434
- width: 32,
1435
- height: 32,
1253
+ return (
1254
+ <Send
1255
+ {...props}
1256
+ containerStyle={{
1436
1257
  alignItems: 'center',
1437
1258
  justifyContent: 'center',
1259
+ marginHorizontal: 4,
1260
+ marginBottom: 0,
1438
1261
  }}
1262
+ disabled={isDisabled}
1439
1263
  >
1440
- <MaterialCommunityIcons
1441
- name="send-circle"
1442
- size={30}
1443
- color={isDisabled ? colors.gray[400] : colors.blue[500]}
1444
- />
1445
- </Box>
1446
- </Send>
1447
- );
1448
- };
1449
-
1450
- const renderMessageText = (props: any) => {
1451
- const { currentMessage } = props;
1452
-
1453
- // For ALERT type messages with call to action
1454
- if (currentMessage.type === 'ALERT') {
1455
- const attachment = currentMessage?.propsConfiguration?.contents?.attachment;
1456
- let action: string = '';
1457
- let actionId: any = '';
1458
- let params: any = {};
1459
- if (attachment?.callToAction?.extraParams) {
1460
- const extraParams: any = attachment?.callToAction?.extraParams;
1461
- const route: any = extraParams?.route ?? null;
1462
- let path: any = null;
1463
- let param: any = null;
1464
- if (role && role == PreDefinedRole.Guest) {
1465
- path = route?.guest?.name ? route?.guest?.name ?? null : null;
1466
- param = route?.guest?.params ? route?.guest?.params ?? null : null;
1467
- } else if (role && role == PreDefinedRole.Owner) {
1468
- path = route?.host?.name ? route?.host?.name ?? null : null;
1469
- param = route?.host?.params ? route?.host?.params ?? null : null;
1470
- } else {
1471
- path = route?.host?.name ? route?.host?.name ?? null : null;
1472
- param = route?.host?.params ? route?.host?.params ?? null : null;
1473
- }
1474
-
1475
- action = path;
1476
- params = { ...param };
1477
- } else if (attachment?.callToAction?.link) {
1478
- action = CALL_TO_ACTION_PATH;
1479
- actionId = attachment?.callToAction?.link.split('/').pop();
1480
- params = { reservationId: actionId };
1481
- }
1482
-
1483
- return (
1484
- <>
1485
- {attachment?.callToAction && action ? (
1486
- <Box className={`bg-[${CALL_TO_ACTION_BOX_BGCOLOR}] rounded-[15] pb-2`}>
1487
- <Button
1488
- variant={'outline'}
1489
- size={'sm'}
1490
- className={`border-[${CALL_TO_ACTION_BUTTON_BORDERCOLOR}]`}
1491
- onPress={() => action && params && navigation.navigate(action, params)}
1492
- >
1493
- <ButtonText className={`color-[${CALL_TO_ACTION_TEXT_COLOR}]`}>
1494
- {attachment.callToAction.title}
1495
- </ButtonText>
1496
- </Button>
1497
- <MessageText
1498
- {...props}
1499
- textStyle={{
1500
- left: { marginLeft: 5, color: CALL_TO_ACTION_TEXT_COLOR, paddingHorizontal: 2 },
1501
- }}
1502
- />
1503
- </Box>
1504
- ) : (
1505
- <MessageText {...props} textStyle={{ left: { marginLeft: 5 } }} />
1506
- )}
1507
- </>
1508
- );
1509
- }
1510
- // For file attachment messages, don't show the "File attachment" text
1511
- else if (currentMessage.text === '📎 File attachment') {
1512
- // Return null to not render any text for these messages
1513
- return null;
1514
- }
1515
- // Default text rendering
1516
- else {
1517
- return <MessageText {...props} textStyle={{ left: { marginLeft: 5 } }} />;
1518
- }
1519
- };
1520
-
1521
- const renderActions = (props) => {
1522
- return (
1523
- <Actions
1524
- {...props}
1525
- options={{
1526
- ['Choose from Library']: onSelectImages,
1527
- ['Cancel']: () => {}, // Add this option to make the sheet dismissible
1528
- }}
1529
- optionTintColor="#000000"
1530
- cancelButtonIndex={1} // Set the Cancel option as the cancel button
1531
- icon={() => (
1532
1264
  <Box
1533
1265
  style={{
1534
1266
  width: 32,
@@ -1537,88 +1269,226 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
1537
1269
  justifyContent: 'center',
1538
1270
  }}
1539
1271
  >
1540
- <Ionicons name="image" size={24} color={colors.blue[500]} />
1272
+ <MaterialCommunityIcons
1273
+ name="send-circle"
1274
+ size={30}
1275
+ color={isDisabled ? colors.gray[400] : colors.blue[500]}
1276
+ />
1541
1277
  </Box>
1542
- )}
1543
- containerStyle={{
1544
- alignItems: 'center',
1545
- justifyContent: 'center',
1546
- marginLeft: 8,
1547
- marginBottom: 0,
1548
- }}
1549
- />
1550
- );
1551
- };
1278
+ </Send>
1279
+ );
1280
+ },
1281
+ [safeContextProperty],
1282
+ );
1283
+
1284
+ const renderActions = useCallback(
1285
+ (props) => {
1286
+ return (
1287
+ <Actions
1288
+ {...props}
1289
+ options={{
1290
+ ['Choose from Library']: onSelectImages,
1291
+ ['Cancel']: () => {}, // Add this option to make the sheet dismissible
1292
+ }}
1293
+ optionTintColor="#000000"
1294
+ cancelButtonIndex={1} // Set the Cancel option as the cancel button
1295
+ icon={() => (
1296
+ <Box
1297
+ style={{
1298
+ width: 32,
1299
+ height: 32,
1300
+ alignItems: 'center',
1301
+ justifyContent: 'center',
1302
+ }}
1303
+ >
1304
+ <Ionicons name="image" size={24} color={colors.blue[500]} />
1305
+ </Box>
1306
+ )}
1307
+ containerStyle={{
1308
+ alignItems: 'center',
1309
+ justifyContent: 'center',
1310
+ marginLeft: 8,
1311
+ marginBottom: 0,
1312
+ }}
1313
+ />
1314
+ );
1315
+ },
1316
+ [onSelectImages],
1317
+ );
1552
1318
 
1553
- const renderAccessory = (props) => {
1554
- const selectedImage = safeContextProperty('selectedImage', '');
1319
+ const renderAccessory = useCallback(
1320
+ (props) => {
1321
+ const selectedImage = safeContextProperty('selectedImage', '');
1555
1322
 
1556
- if (!selectedImage) {
1557
- return null;
1558
- }
1323
+ if (!selectedImage) {
1324
+ return null;
1325
+ }
1559
1326
 
1560
- return (
1561
- <View
1562
- style={{
1563
- height: 80,
1564
- padding: 10,
1565
- backgroundColor: 'white',
1566
- borderTopWidth: 1,
1567
- borderTopColor: '#e0e0e0',
1568
- flexDirection: 'row',
1569
- alignItems: 'center',
1570
- }}
1571
- >
1327
+ return (
1572
1328
  <View
1573
1329
  style={{
1574
- flex: 1,
1330
+ height: 80,
1331
+ padding: 10,
1332
+ backgroundColor: 'white',
1333
+ borderTopWidth: 1,
1334
+ borderTopColor: '#e0e0e0',
1575
1335
  flexDirection: 'row',
1576
1336
  alignItems: 'center',
1577
- // justifyContent: 'space-between',
1578
- paddingHorizontal: 20,
1579
1337
  }}
1580
1338
  >
1581
- <Image
1582
- key={state?.context?.selectedImage}
1583
- alt={'selected image'}
1584
- source={{ uri: state?.context?.selectedImage }}
1585
- size={'xs'}
1586
- style={{
1587
- width: 5,
1588
- height: 5,
1589
- borderRadius: 5,
1590
- marginRight: 20,
1591
- }}
1592
- />
1593
-
1594
- <TouchableHighlight
1595
- underlayColor="#dddddd"
1596
- onPress={() => safeSend({ type: ThreadActions.CLEAR_IMAGE })}
1339
+ <View
1597
1340
  style={{
1598
- backgroundColor: '#f44336',
1599
- paddingVertical: 2,
1600
- paddingHorizontal: 5,
1601
- borderRadius: 5,
1602
- marginLeft: 10,
1603
- elevation: 3,
1604
- shadowColor: '#000',
1605
- shadowOffset: { width: 0, height: 1 },
1606
- shadowOpacity: 0.3,
1607
- shadowRadius: 2,
1341
+ flex: 1,
1342
+ flexDirection: 'row',
1343
+ alignItems: 'center',
1344
+ paddingHorizontal: 20,
1608
1345
  }}
1609
1346
  >
1610
- <Text style={{ color: 'white', fontWeight: 'bold' }}>X</Text>
1611
- </TouchableHighlight>
1347
+ <Image
1348
+ key={state?.context?.selectedImage}
1349
+ alt={'selected image'}
1350
+ source={{ uri: state?.context?.selectedImage }}
1351
+ size={'xs'}
1352
+ style={{
1353
+ width: 5,
1354
+ height: 5,
1355
+ borderRadius: 5,
1356
+ marginRight: 20,
1357
+ }}
1358
+ />
1359
+
1360
+ <TouchableHighlight
1361
+ underlayColor="#dddddd"
1362
+ onPress={() => safeSend({ type: ThreadActions.CLEAR_IMAGE })}
1363
+ style={{
1364
+ backgroundColor: '#f44336',
1365
+ paddingVertical: 2,
1366
+ paddingHorizontal: 5,
1367
+ borderRadius: 5,
1368
+ marginLeft: 10,
1369
+ elevation: 3,
1370
+ shadowColor: '#000',
1371
+ shadowOffset: { width: 0, height: 1 },
1372
+ shadowOpacity: 0.3,
1373
+ shadowRadius: 2,
1374
+ }}
1375
+ >
1376
+ <Text style={{ color: 'white', fontWeight: 'bold' }}>X</Text>
1377
+ </TouchableHighlight>
1378
+ </View>
1612
1379
  </View>
1613
- </View>
1380
+ );
1381
+ },
1382
+ [state?.context?.selectedImage, safeSend],
1383
+ );
1384
+
1385
+ const renderInputToolbar = useCallback((props) => {
1386
+ return (
1387
+ <InputToolbar
1388
+ {...props}
1389
+ containerStyle={{
1390
+ backgroundColor: 'white',
1391
+ borderTopWidth: 1,
1392
+ borderTopColor: colors.gray[200],
1393
+ paddingHorizontal: 4,
1394
+ paddingVertical: 4,
1395
+ }}
1396
+ primaryStyle={{
1397
+ alignItems: 'center',
1398
+ }}
1399
+ />
1614
1400
  );
1615
- };
1401
+ }, []);
1616
1402
 
1617
1403
  const setImageViewerObject = (obj: any, v: boolean) => {
1618
1404
  setImageObject(obj);
1619
1405
  setImageViewer(v);
1620
1406
  };
1621
1407
 
1408
+ // Add back the renderMessageText function (memoized)
1409
+ const renderMessageText = useCallback(
1410
+ (props: any) => {
1411
+ const { currentMessage } = props;
1412
+
1413
+ // For ALERT type messages with call to action
1414
+ if (currentMessage.type === 'ALERT') {
1415
+ const attachment = currentMessage?.propsConfiguration?.contents?.attachment;
1416
+ let action: string = '';
1417
+ let actionId: any = '';
1418
+ let params: any = {};
1419
+ if (attachment?.callToAction?.extraParams) {
1420
+ const extraParams: any = attachment?.callToAction?.extraParams;
1421
+ const route: any = extraParams?.route ?? null;
1422
+ let path: any = null;
1423
+ let param: any = null;
1424
+ if (role && role == PreDefinedRole.Guest) {
1425
+ path = route?.guest?.name ? route?.guest?.name ?? null : null;
1426
+ param = route?.guest?.params ? route?.guest?.params ?? null : null;
1427
+ } else if (role && role == PreDefinedRole.Owner) {
1428
+ path = route?.host?.name ? route?.host?.name ?? null : null;
1429
+ param = route?.host?.params ? route?.host?.params ?? null : null;
1430
+ } else {
1431
+ path = route?.host?.name ? route?.host?.name ?? null : null;
1432
+ param = route?.host?.params ? route?.host?.params ?? null : null;
1433
+ }
1434
+
1435
+ action = path;
1436
+ params = { ...param };
1437
+ } else if (attachment?.callToAction?.link) {
1438
+ action = CALL_TO_ACTION_PATH;
1439
+ actionId = attachment?.callToAction?.link.split('/').pop();
1440
+ params = { reservationId: actionId };
1441
+ }
1442
+
1443
+ return (
1444
+ <>
1445
+ {attachment?.callToAction && action ? (
1446
+ <Box className={`bg-[${CALL_TO_ACTION_BOX_BGCOLOR}] rounded-[15] pb-2`}>
1447
+ <Button
1448
+ variant={'outline'}
1449
+ size={'sm'}
1450
+ className={`border-[${CALL_TO_ACTION_BUTTON_BORDERCOLOR}]`}
1451
+ onPress={() => action && params && navigation.navigate(action, params)}
1452
+ >
1453
+ <ButtonText className={`color-[${CALL_TO_ACTION_TEXT_COLOR}]`}>
1454
+ {attachment.callToAction.title}
1455
+ </ButtonText>
1456
+ </Button>
1457
+ <MessageText
1458
+ {...props}
1459
+ textStyle={{
1460
+ left: { marginLeft: 5, color: CALL_TO_ACTION_TEXT_COLOR, paddingHorizontal: 2 },
1461
+ }}
1462
+ />
1463
+ </Box>
1464
+ ) : (
1465
+ <MessageText {...props} textStyle={{ left: { marginLeft: 5 } }} />
1466
+ )}
1467
+ </>
1468
+ );
1469
+ }
1470
+ // For file attachment messages, don't show the "File attachment" text
1471
+ else if (currentMessage.text === '📎 File attachment') {
1472
+ // Return null to not render any text for these messages
1473
+ return null;
1474
+ }
1475
+ // Default text rendering
1476
+ else {
1477
+ return <MessageText {...props} textStyle={{ left: { marginLeft: 5 } }} />;
1478
+ }
1479
+ },
1480
+ [role, navigation],
1481
+ );
1482
+
1483
+ const renderMessage = useCallback(
1484
+ (props: any) => {
1485
+ return (
1486
+ <SlackMessage {...props} isShowImageViewer={isShowImageViewer} setImageViewer={setImageViewerObject} />
1487
+ );
1488
+ },
1489
+ [isShowImageViewer, setImageViewerObject],
1490
+ );
1491
+
1622
1492
  const modalContent = React.useMemo(() => {
1623
1493
  if (!imageObject) return <></>;
1624
1494
  const { image, _id } = imageObject;
@@ -1636,35 +1506,12 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
1636
1506
  );
1637
1507
  }, [imageObject]);
1638
1508
 
1639
- const renderMessage = (props: any) => {
1640
- return <SlackMessage {...props} isShowImageViewer={isShowImageViewer} setImageViewer={setImageViewerObject} />;
1641
- };
1642
-
1643
1509
  // Define a memo that provides the current message text from state
1644
1510
  const currentMessageText = useMemo(() => {
1645
1511
  const text = safeContextProperty('messageText', '') || ' ';
1646
1512
  return text;
1647
1513
  }, [safeContextProperty('messageText')]);
1648
1514
 
1649
- // Define a custom renderInputToolbar function
1650
- const renderInputToolbar = (props) => {
1651
- return (
1652
- <InputToolbar
1653
- {...props}
1654
- containerStyle={{
1655
- backgroundColor: 'white',
1656
- borderTopWidth: 1,
1657
- borderTopColor: colors.gray[200],
1658
- paddingHorizontal: 4,
1659
- paddingVertical: 4,
1660
- }}
1661
- primaryStyle={{
1662
- alignItems: 'center',
1663
- }}
1664
- />
1665
- );
1666
- };
1667
-
1668
1515
  // Add a function to load messages with specific skip value for debugging
1669
1516
  const forceLoadMessages = useCallback(
1670
1517
  (skipValue: number) => {
@@ -1721,6 +1568,144 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
1721
1568
  [channelId, parentId, getThreadMessages, safeSend],
1722
1569
  );
1723
1570
 
1571
+ // Optimize the onFetchOld function to be more efficient
1572
+ const onFetchOld = useCallback(() => {
1573
+ const totalCount = safeContextProperty('totalCount', 0);
1574
+ const threadMessages = safeContextProperty('threadMessages', []);
1575
+ const isLoading = safeContextProperty('loadingOldMessages', false);
1576
+
1577
+ if (totalCount <= threadMessages.length || isLoading) {
1578
+ // Make sure we're not stuck in loading state
1579
+ if (isLoading) {
1580
+ safeSend({
1581
+ type: ThreadActions.STOP_LOADING,
1582
+ data: { loadingOldMessages: false },
1583
+ });
1584
+ }
1585
+ if (__DEV__) console.log('No more messages to load or already loading');
1586
+ return;
1587
+ }
1588
+
1589
+ if (__DEV__) console.log('Loading more messages - current count:', threadMessages.length, 'of', totalCount);
1590
+
1591
+ // Set the loading state specifically for old messages
1592
+ safeSend({
1593
+ type: ThreadActions.FETCH_MORE_MESSAGES,
1594
+ data: { loadingOldMessages: true },
1595
+ });
1596
+
1597
+ // Use simple, consistent approach: Skip=0, Limit=50
1598
+ if (__DEV__) console.log('Using proven approach: Skip=0, Limit=50');
1599
+
1600
+ // Create query variables once
1601
+ const queryVariables = {
1602
+ channelId: channelId?.toString(),
1603
+ role: role?.toString(),
1604
+ postParentId: !parentId || parentId == 0 ? null : parentId?.toString(),
1605
+ selectedFields: 'id channel post replies replyCount lastReplyAt createdAt updatedAt',
1606
+ limit: 50,
1607
+ skip: 0,
1608
+ };
1609
+
1610
+ if (__DEV__) console.log('Query variables:', JSON.stringify(queryVariables));
1611
+
1612
+ fetchMoreMessages({
1613
+ variables: queryVariables,
1614
+ })
1615
+ .then((res: any) => {
1616
+ if (__DEV__) console.log('API response received with status:', res ? 'success' : 'empty');
1617
+
1618
+ if (res?.data?.getPostThread) {
1619
+ const threads: any = res?.data?.getPostThread;
1620
+ const threadReplies = threads?.replies ?? [];
1621
+ const actualTotalCount = threads?.replyCount ?? 0;
1622
+
1623
+ if (__DEV__) {
1624
+ console.log('API response details:');
1625
+ console.log('- replyCount:', threads?.replyCount);
1626
+ console.log('- replies array length:', threadReplies.length);
1627
+ }
1628
+
1629
+ if (threadReplies.length > 0) {
1630
+ // Compare with our existing messages to find the ones we're missing
1631
+ const existingIds = new Set(threadMessages.map((msg) => msg.id));
1632
+ const newUniqueMessages = threadReplies.filter((msg) => !existingIds.has(msg.id));
1633
+
1634
+ if (__DEV__)
1635
+ console.log(
1636
+ `Found ${newUniqueMessages.length} unique new messages out of ${threadReplies.length} received`,
1637
+ );
1638
+
1639
+ // If no new unique messages, it means we already have everything
1640
+ if (newUniqueMessages.length === 0) {
1641
+ if (__DEV__) console.log('No new unique messages found, adjusting total count');
1642
+
1643
+ // Register this as a failed load attempt
1644
+ registerLoadAttemptResult(false);
1645
+
1646
+ // Adjust total count to match what we have
1647
+ safeSend({
1648
+ type: ThreadActions.SET_THREAD_MESSAGES,
1649
+ data: {
1650
+ messages: threadMessages,
1651
+ totalCount: threadMessages.length,
1652
+ threadPost: safeContextProperty('threadPost', []),
1653
+ postThread: safeContextProperty('postThread', null),
1654
+ },
1655
+ });
1656
+ return;
1657
+ }
1658
+
1659
+ // Register success since we found new messages
1660
+ registerLoadAttemptResult(true);
1661
+
1662
+ if (__DEV__) console.log(`Adding ${newUniqueMessages.length} new messages to thread`);
1663
+
1664
+ safeSend({
1665
+ type: 'FETCH_MORE_MESSAGES_SUCCESS',
1666
+ data: {
1667
+ messages: newUniqueMessages,
1668
+ totalCount: actualTotalCount,
1669
+ loadingOldMessages: false,
1670
+ },
1671
+ });
1672
+ } else {
1673
+ if (__DEV__) console.log('No thread replies returned when loading more messages');
1674
+ registerLoadAttemptResult(false);
1675
+
1676
+ // Adjust total count to match what we have, since server says there are no more messages
1677
+ safeSend({
1678
+ type: ThreadActions.SET_THREAD_MESSAGES,
1679
+ data: {
1680
+ messages: threadMessages,
1681
+ totalCount: threadMessages.length,
1682
+ threadPost: safeContextProperty('threadPost', []),
1683
+ postThread: safeContextProperty('postThread', null),
1684
+ },
1685
+ });
1686
+ }
1687
+ } else {
1688
+ if (__DEV__) console.log('No thread data returned when loading more messages');
1689
+ registerLoadAttemptResult(false);
1690
+ safeSend({
1691
+ type: ThreadActions.STOP_LOADING,
1692
+ data: { loadingOldMessages: false },
1693
+ });
1694
+ }
1695
+ })
1696
+ .catch((error: any) => {
1697
+ console.error('Error fetching more messages:', error);
1698
+ registerLoadAttemptResult(false);
1699
+ safeSend({
1700
+ type: 'ERROR',
1701
+ data: {
1702
+ message: error.message,
1703
+ loadingOldMessages: false,
1704
+ },
1705
+ });
1706
+ });
1707
+ }, [parentId, channelId, role, safeContextProperty, safeSend, fetchMoreMessages, registerLoadAttemptResult]);
1708
+
1724
1709
  return (
1725
1710
  <SafeAreaView style={{ flex: 1 }}>
1726
1711
  {safeContextProperty('loadingOldMessages', false) === true && (
@@ -2104,6 +2089,7 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
2104
2089
  scrollEventThrottle: 16,
2105
2090
  keyboardDismissMode: 'on-drag',
2106
2091
  keyboardShouldPersistTaps: 'handled',
2092
+ // removeClippedSubviews={true}
2107
2093
  }}
2108
2094
  onSend={(messages) => {
2109
2095
  if (!messages || messages.length === 0) {
@@ -2116,7 +2102,7 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
2116
2102
  const currentInputText = currentMessageText;
2117
2103
  const messageToSend = currentInputText?.trim() || messages[0]?.text?.trim() || ' ';
2118
2104
 
2119
- console.log('GiftedChat onSend triggered with text from state:', messageToSend);
2105
+ if (__DEV__) console.log('GiftedChat onSend triggered with text from state:', messageToSend);
2120
2106
 
2121
2107
  // Make sure we update the message text in state
2122
2108
  safeSend({
@@ -2126,18 +2112,19 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
2126
2112
 
2127
2113
  // Then send the message
2128
2114
  if (safeContextProperty('images', []).length > 0) {
2129
- console.log(
2130
- 'Sending message with file:',
2131
- messageToSend,
2132
- 'Images:',
2133
- safeContextProperty('images', []).length,
2134
- );
2115
+ if (__DEV__)
2116
+ console.log(
2117
+ 'Sending message with file:',
2118
+ messageToSend,
2119
+ 'Images:',
2120
+ safeContextProperty('images', []).length,
2121
+ );
2135
2122
  safeSend({
2136
2123
  type: ThreadActions.SEND_THREAD_MESSAGE_WITH_FILE,
2137
2124
  data: { messageText: messageToSend },
2138
2125
  });
2139
2126
  } else {
2140
- console.log('Sending text message:', messageToSend);
2127
+ if (__DEV__) console.log('Sending text message:', messageToSend);
2141
2128
  safeSend({
2142
2129
  type: ThreadActions.SEND_THREAD_MESSAGE,
2143
2130
  data: { messageText: messageToSend },
@@ -2146,12 +2133,7 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
2146
2133
  }}
2147
2134
  text={currentMessageText}
2148
2135
  onInputTextChanged={(text) => {
2149
- // Don't log every keystroke to reduce console spam
2150
- if (text.length % 5 === 0 || text.length < 5) {
2151
- console.log('Input text changed:', text);
2152
- }
2153
-
2154
- // Set the text in the state
2136
+ // Set the text in the state without excessive logging
2155
2137
  safeSend({
2156
2138
  type: ThreadActions.SET_MESSAGE_TEXT,
2157
2139
  data: { messageText: text },
@@ -2176,7 +2158,7 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
2176
2158
  user={{
2177
2159
  _id: auth?.id || '',
2178
2160
  }}
2179
- isTyping={true}
2161
+ isTyping={safeContextProperty('loading', false)}
2180
2162
  alwaysShowSend={safeContextProperty('loading', false) ? false : true}
2181
2163
  infiniteScroll={true}
2182
2164
  renderSend={renderSend}
@@ -2185,6 +2167,7 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
2185
2167
  renderActions={renderActions}
2186
2168
  renderAccessory={!!state?.context?.selectedImage ? renderAccessory : undefined}
2187
2169
  renderMessage={renderMessage}
2170
+ renderMessageText={renderMessageText}
2188
2171
  maxInputLength={1000}
2189
2172
  placeholder="Type a message..."
2190
2173
  showUserAvatar={true}
@@ -2249,7 +2232,7 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
2249
2232
  if (!subscriptionData.data) return prev;
2250
2233
  const newMessage: any = subscriptionData?.data?.threadChatMessageAdded;
2251
2234
  const prevReplyCount: any = prev?.getPostThread?.replyCount;
2252
- const newReplyCount = prevReplyCount || 0 + 1;
2235
+ const newReplyCount = (prevReplyCount || 0) + 1;
2253
2236
  const replies = prev?.getPostThread?.replies || [];
2254
2237
 
2255
2238
  safeSend({
@@ -2280,7 +2263,7 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
2280
2263
  />
2281
2264
  </>
2282
2265
  )}
2283
- messagesContainerStyle={messageList?.length == 0 && { transform: [{ scaleY: -1 }] }}
2266
+ messagesContainerStyle={messageList?.length == 0 ? { transform: [{ scaleY: -1 }] } : undefined}
2284
2267
  renderChatEmpty={() => (
2285
2268
  <>
2286
2269
  {!threadLoading && messageList && messageList?.length == 0 && (