stream-chat-react-native-core 5.22.2-beta.8 → 5.23.0-beta.2

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 (62) hide show
  1. package/lib/commonjs/components/Channel/Channel.js +587 -384
  2. package/lib/commonjs/components/Channel/Channel.js.map +1 -1
  3. package/lib/commonjs/components/MessageList/MessageList.js +170 -179
  4. package/lib/commonjs/components/MessageList/MessageList.js.map +1 -1
  5. package/lib/commonjs/components/MessageList/hooks/useMessageList.js +6 -1
  6. package/lib/commonjs/components/MessageList/hooks/useMessageList.js.map +1 -1
  7. package/lib/commonjs/components/MessageList/hooks/useShouldScrollToRecentOnNewOwnMessage.js +36 -0
  8. package/lib/commonjs/components/MessageList/hooks/useShouldScrollToRecentOnNewOwnMessage.js.map +1 -0
  9. package/lib/commonjs/contexts/channelsStateContext/ChannelsStateContext.js +1 -1
  10. package/lib/commonjs/contexts/channelsStateContext/ChannelsStateContext.js.map +1 -1
  11. package/lib/commonjs/contexts/messageInputContext/MessageInputContext.js +31 -15
  12. package/lib/commonjs/contexts/messageInputContext/MessageInputContext.js.map +1 -1
  13. package/lib/commonjs/i18n/fr.json +15 -15
  14. package/lib/commonjs/i18n/hi.json +15 -15
  15. package/lib/commonjs/i18n/it.json +15 -15
  16. package/lib/commonjs/i18n/nl.json +15 -15
  17. package/lib/commonjs/i18n/ru.json +15 -15
  18. package/lib/commonjs/i18n/tr.json +15 -15
  19. package/lib/commonjs/version.json +1 -1
  20. package/lib/module/components/Channel/Channel.js +587 -384
  21. package/lib/module/components/Channel/Channel.js.map +1 -1
  22. package/lib/module/components/MessageList/MessageList.js +170 -179
  23. package/lib/module/components/MessageList/MessageList.js.map +1 -1
  24. package/lib/module/components/MessageList/hooks/useMessageList.js +6 -1
  25. package/lib/module/components/MessageList/hooks/useMessageList.js.map +1 -1
  26. package/lib/module/components/MessageList/hooks/useShouldScrollToRecentOnNewOwnMessage.js +36 -0
  27. package/lib/module/components/MessageList/hooks/useShouldScrollToRecentOnNewOwnMessage.js.map +1 -0
  28. package/lib/module/contexts/channelsStateContext/ChannelsStateContext.js +1 -1
  29. package/lib/module/contexts/channelsStateContext/ChannelsStateContext.js.map +1 -1
  30. package/lib/module/contexts/messageInputContext/MessageInputContext.js +31 -15
  31. package/lib/module/contexts/messageInputContext/MessageInputContext.js.map +1 -1
  32. package/lib/module/i18n/fr.json +15 -15
  33. package/lib/module/i18n/hi.json +15 -15
  34. package/lib/module/i18n/it.json +15 -15
  35. package/lib/module/i18n/nl.json +15 -15
  36. package/lib/module/i18n/ru.json +15 -15
  37. package/lib/module/i18n/tr.json +15 -15
  38. package/lib/module/version.json +1 -1
  39. package/lib/typescript/components/MessageList/hooks/useMessageList.d.ts +6 -1
  40. package/lib/typescript/components/MessageList/hooks/useShouldScrollToRecentOnNewOwnMessage.d.ts +4 -0
  41. package/lib/typescript/i18n/fr.json +15 -15
  42. package/lib/typescript/i18n/hi.json +15 -15
  43. package/lib/typescript/i18n/it.json +15 -15
  44. package/lib/typescript/i18n/nl.json +15 -15
  45. package/lib/typescript/i18n/ru.json +15 -15
  46. package/lib/typescript/i18n/tr.json +15 -15
  47. package/package.json +1 -1
  48. package/src/components/Channel/Channel.tsx +237 -61
  49. package/src/components/MessageList/MessageList.tsx +190 -180
  50. package/src/components/MessageList/__tests__/useMessageList.test.tsx +5 -2
  51. package/src/components/MessageList/hooks/useMessageList.ts +8 -1
  52. package/src/components/MessageList/hooks/useShouldScrollToRecentOnNewOwnMessage.ts +44 -0
  53. package/src/contexts/__tests__/index.test.tsx +1 -1
  54. package/src/contexts/channelsStateContext/ChannelsStateContext.tsx +1 -1
  55. package/src/contexts/messageInputContext/MessageInputContext.tsx +7 -1
  56. package/src/i18n/fr.json +15 -15
  57. package/src/i18n/hi.json +15 -15
  58. package/src/i18n/it.json +15 -15
  59. package/src/i18n/nl.json +15 -15
  60. package/src/i18n/ru.json +15 -15
  61. package/src/i18n/tr.json +15 -15
  62. package/src/version.json +1 -1
@@ -9,11 +9,15 @@ import {
9
9
  ViewToken,
10
10
  } from 'react-native';
11
11
 
12
+ import type { FormatMessageResponse } from 'stream-chat';
13
+
12
14
  import {
13
15
  isMessageWithStylesReadByAndDateSeparator,
14
16
  MessageType,
15
17
  useMessageList,
16
18
  } from './hooks/useMessageList';
19
+ import { useShouldScrollToRecentOnNewOwnMessage } from './hooks/useShouldScrollToRecentOnNewOwnMessage';
20
+
17
21
  import { InlineLoadingMoreIndicator } from './InlineLoadingMoreIndicator';
18
22
  import { InlineLoadingMoreRecentIndicator } from './InlineLoadingMoreRecentIndicator';
19
23
  import { InlineLoadingMoreThreadIndicator } from './InlineLoadingMoreThreadIndicator';
@@ -298,21 +302,33 @@ const MessageListWithContext = <
298
302
  [myMessageTheme, theme],
299
303
  );
300
304
 
301
- const messageList = useMessageList<StreamChatGenerics>({
305
+ /**
306
+ * NOTE: rawMessageList changes only when messages array state changes
307
+ * processedMessageList changes on any state change
308
+ */
309
+ const { processedMessageList, rawMessageList } = useMessageList<StreamChatGenerics>({
302
310
  noGroupByUser,
303
311
  threadList,
304
312
  });
305
313
  const messageListLengthBeforeUpdate = useRef(0);
306
- const messageListLengthAfterUpdate = messageList.length;
314
+ const messageListLengthAfterUpdate = processedMessageList.length;
307
315
 
308
316
  /**
309
317
  * We need topMessage and channelLastRead values to set the initial scroll position.
310
318
  * So these values only get used if `initialScrollToFirstUnreadMessage` prop is true.
311
319
  */
312
- const topMessageBeforeUpdate = useRef<MessageType<StreamChatGenerics>>();
313
- const topMessageAfterUpdate = messageList[messageList.length - 1];
320
+ const topMessageBeforeUpdate = useRef<FormatMessageResponse<StreamChatGenerics>>();
321
+ const latestNonCurrentMessageBeforeUpdateRef =
322
+ useRef<FormatMessageResponse<StreamChatGenerics>>();
323
+ const topMessageAfterUpdate: FormatMessageResponse<StreamChatGenerics> | undefined =
324
+ rawMessageList[0];
325
+
326
+ const shouldScrollToRecentOnNewOwnMessageRef = useShouldScrollToRecentOnNewOwnMessage(
327
+ rawMessageList,
328
+ client.userID,
329
+ );
314
330
 
315
- const [autoscrollToTop, setAutoscrollToTop] = useState(false);
331
+ const [autoscrollToRecent, setAutoscrollToRecent] = useState(false);
316
332
 
317
333
  /**
318
334
  * We want to call onEndReached and onStartReached only once, per content length.
@@ -347,20 +363,17 @@ const MessageListWithContext = <
347
363
  /**
348
364
  * The timeout id used to temporarily load the initial scroll set flag
349
365
  */
350
- const tempDisablePaginationTrackersTimeoutRef = useRef<NodeJS.Timeout>();
366
+ const onScrollEventTimeoutRef = useRef<NodeJS.Timeout>();
351
367
 
352
- /**
353
- * If a messageId was requested to scroll to but was unloaded,
354
- * this flag keeps track of it to scroll to it after loading the message
355
- */
356
- const messageIdToScrollToRef = useRef<string>();
357
368
  /**
358
369
  * Last messageID that was scrolled to after loading a new message list,
359
370
  * this flag keeps track of it so that we dont scroll to it again on target message set
360
371
  */
361
372
  const messageIdLastScrolledToRef = useRef<string>();
362
373
  const [hasMoved, setHasMoved] = useState(false);
363
- const [lastReceivedId, setLastReceivedId] = useState(getLastReceivedMessage(messageList)?.id);
374
+ const [lastReceivedId, setLastReceivedId] = useState(
375
+ getLastReceivedMessage(processedMessageList)?.id,
376
+ );
364
377
  const [scrollToBottomButtonVisible, setScrollToBottomButtonVisible] = useState(false);
365
378
 
366
379
  const [stickyHeaderDate, setStickyHeaderDate] = useState<Date | undefined>();
@@ -426,22 +439,6 @@ const MessageListWithContext = <
426
439
  onEndReachedTracker.current = {};
427
440
  });
428
441
 
429
- /**
430
- * Disables the pagination trackers for a second
431
- * This is used to prevent the onEndReached and onStartReached from firing
432
- * when we scroll to the bottom or top of the list automatically without user interaction
433
- * Ex: for targeted message scroll
434
- */
435
- const tempDisablePaginationTrackersRef = useRef((messageListLength: number) => {
436
- clearTimeout(tempDisablePaginationTrackersTimeoutRef.current);
437
- onStartReachedTracker.current[messageListLength] = true;
438
- onEndReachedTracker.current[messageListLength] = true;
439
- tempDisablePaginationTrackersTimeoutRef.current = setTimeout(() => {
440
- onStartReachedTracker.current[messageListLength] = false;
441
- onEndReachedTracker.current[messageListLength] = false;
442
- }, 1000);
443
- });
444
-
445
442
  useEffect(() => {
446
443
  setScrollToBottomButtonVisible(false);
447
444
  }, [disabled]);
@@ -482,29 +479,21 @@ const MessageListWithContext = <
482
479
  }, [loading, scrollToBottomButtonVisible, isInitialScrollDone]);
483
480
 
484
481
  useEffect(() => {
485
- const lastReceivedMessage = getLastReceivedMessage(messageList);
486
-
487
- const hasNewMessage = lastReceivedId !== lastReceivedMessage?.id;
488
- const isMyMessage = lastReceivedMessage?.user?.id === client.userID;
489
-
482
+ const lastReceivedMessage = getLastReceivedMessage(processedMessageList);
490
483
  setLastReceivedId(lastReceivedMessage?.id);
491
484
 
492
485
  /**
493
486
  * Scroll down when
494
- * 1. you send a new message to channel
495
- * 2. new message list is small than the one before update - channel has resynced
496
- * 3. created_at timestamp of top message before update is lesser than created_at timestamp of top message after update - channel has resynced
487
+ * created_at timestamp of top message before update is lesser than created_at timestamp of top message after update - channel has resynced
497
488
  */
498
489
  const scrollToBottomIfNeeded = () => {
499
- if (!client || !channel || messageList.length === 0) {
490
+ if (!client || !channel || rawMessageList.length === 0) {
500
491
  return;
501
492
  }
502
493
  if (
503
- (hasNewMessage && isMyMessage) ||
504
- messageListLengthAfterUpdate < messageListLengthBeforeUpdate.current ||
505
- (topMessageBeforeUpdate.current?.created_at &&
506
- topMessageAfterUpdate?.created_at &&
507
- topMessageBeforeUpdate.current.created_at < topMessageAfterUpdate.created_at)
494
+ topMessageBeforeUpdate.current?.created_at &&
495
+ topMessageAfterUpdate?.created_at &&
496
+ topMessageBeforeUpdate.current.created_at < topMessageAfterUpdate.created_at
508
497
  ) {
509
498
  channelResyncScrollSet.current = false;
510
499
  setScrollToBottomButtonVisible(false);
@@ -545,12 +534,52 @@ const MessageListWithContext = <
545
534
 
546
535
  messageListLengthBeforeUpdate.current = messageListLengthAfterUpdate;
547
536
  topMessageBeforeUpdate.current = topMessageAfterUpdate;
548
- }, [hasNoMoreRecentMessagesToLoad, messageListLengthAfterUpdate, topMessageAfterUpdate?.id]);
537
+ }, [
538
+ threadList,
539
+ hasNoMoreRecentMessagesToLoad,
540
+ messageListLengthAfterUpdate,
541
+ topMessageAfterUpdate?.id,
542
+ ]);
549
543
 
550
544
  useEffect(() => {
551
- setAutoscrollToTop(hasNoMoreRecentMessagesToLoad);
552
- }, [messageList, hasNoMoreRecentMessagesToLoad]);
545
+ if (!rawMessageList.length) return;
546
+ const notLatestSet = !threadList && channel.state.messages !== channel.state.latestMessages;
547
+ if (notLatestSet) {
548
+ latestNonCurrentMessageBeforeUpdateRef.current =
549
+ channel.state.latestMessages[channel.state.latestMessages.length - 1];
550
+ setAutoscrollToRecent(false);
551
+ setScrollToBottomButtonVisible(true);
552
+ return;
553
+ }
554
+ const latestNonCurrentMessageBeforeUpdate = latestNonCurrentMessageBeforeUpdateRef.current;
555
+ latestNonCurrentMessageBeforeUpdateRef.current = undefined;
556
+ const latestCurrentMessageAfterUpdate = rawMessageList[rawMessageList.length - 1];
557
+ if (!latestCurrentMessageAfterUpdate) {
558
+ setAutoscrollToRecent(true);
559
+ return;
560
+ }
561
+ const didMergeMessageSetsWithNoUpdates =
562
+ latestNonCurrentMessageBeforeUpdate?.id === latestCurrentMessageAfterUpdate.id;
563
+ // if didMergeMessageSetsWithNoUpdates=false, we got new messages
564
+ // so we should scroll to bottom if we are near the bottom already
565
+ setAutoscrollToRecent(!didMergeMessageSetsWithNoUpdates);
566
+
567
+ if (!didMergeMessageSetsWithNoUpdates) {
568
+ const shouldScrollToRecentOnNewOwnMessage = shouldScrollToRecentOnNewOwnMessageRef.current();
569
+ // we should scroll to bottom where ever we are now
570
+ // as we have sent a new own message
571
+ if (shouldScrollToRecentOnNewOwnMessage) {
572
+ setTimeout(() => {
573
+ flatListRef.current?.scrollToOffset({
574
+ animated: true,
575
+ offset: 0,
576
+ });
577
+ }, 150); // flatlist might take a bit to update, so a small delay is needed
578
+ }
579
+ }
580
+ }, [rawMessageList, threadList]);
553
581
 
582
+ // TODO: do not apply on RN 0.73 and above
554
583
  const shouldApplyAndroidWorkaround = inverted && Platform.OS === 'android';
555
584
 
556
585
  const renderItem = ({
@@ -563,14 +592,27 @@ const MessageListWithContext = <
563
592
  if (!channel || channel.disconnected || (!channel.initialized && !channel.offlineMode))
564
593
  return null;
565
594
 
595
+ const unreadCount = channel.countUnread();
566
596
  const lastRead = channel.lastRead();
567
597
 
568
598
  function isMessageUnread(messageArrayIndex: number): boolean {
569
- const msg = messageList?.[messageArrayIndex];
570
- if (lastRead && msg?.created_at) {
571
- return lastRead < msg.created_at;
599
+ const isLatestMessageSetShown = !!channel.state.messageSets.find(
600
+ (set) => set.isCurrent && set.isLatest,
601
+ );
602
+ const msg = processedMessageList?.[messageArrayIndex];
603
+ if (!isLatestMessageSetShown) {
604
+ if (
605
+ channel.state.latestMessages.length !== 0 &&
606
+ unreadCount > channel.state.latestMessages.length
607
+ ) {
608
+ return messageArrayIndex <= unreadCount - channel.state.latestMessages.length - 1;
609
+ } else if (lastRead && msg.created_at) {
610
+ return lastRead < msg.created_at;
611
+ }
612
+ return false;
613
+ } else {
614
+ return messageArrayIndex <= unreadCount - 1;
572
615
  }
573
- return false;
574
616
  }
575
617
 
576
618
  const isCurrentMessageUnread = isMessageUnread(index);
@@ -593,61 +635,40 @@ const MessageListWithContext = <
593
635
  }
594
636
 
595
637
  const wrapMessageInTheme = client.userID === message.user?.id && !!myMessageTheme;
638
+ const renderDateSeperator = isMessageWithStylesReadByAndDateSeparator(message) &&
639
+ message.dateSeparator && <InlineDateSeparator date={message.dateSeparator} />;
640
+ const renderMessage = (
641
+ <Message
642
+ goToMessage={goToMessage}
643
+ groupStyles={isMessageWithStylesReadByAndDateSeparator(message) ? message.groupStyles : []}
644
+ isTargetedMessage={targetedMessage === message.id}
645
+ lastReceivedId={
646
+ lastReceivedId === message.id || message.quoted_message_id ? lastReceivedId : undefined
647
+ }
648
+ message={message}
649
+ onThreadSelect={onThreadSelect}
650
+ showUnreadUnderlay={showUnreadUnderlay}
651
+ style={[{ paddingHorizontal: screenPadding }, messageContainer]}
652
+ threadList={threadList}
653
+ />
654
+ );
596
655
  return wrapMessageInTheme ? (
597
656
  <>
598
- {shouldApplyAndroidWorkaround &&
599
- isMessageWithStylesReadByAndDateSeparator(message) &&
600
- message.dateSeparator && <InlineDateSeparator date={message.dateSeparator} />}
657
+ {shouldApplyAndroidWorkaround && renderDateSeperator}
601
658
  <ThemeProvider mergedStyle={modifiedTheme}>
602
- <View testID={`message-list-item-${index}`}>
603
- <Message
604
- goToMessage={goToMessage}
605
- groupStyles={
606
- isMessageWithStylesReadByAndDateSeparator(message) ? message.groupStyles : []
607
- }
608
- isTargetedMessage={targetedMessage === message.id}
609
- lastReceivedId={lastReceivedId === message.id ? lastReceivedId : undefined}
610
- message={message}
611
- onThreadSelect={onThreadSelect}
612
- showUnreadUnderlay={showUnreadUnderlay}
613
- style={[{ paddingHorizontal: screenPadding }, messageContainer]}
614
- threadList={threadList}
615
- />
616
- </View>
659
+ <View testID={`message-list-item-${index}`}>{renderMessage}</View>
617
660
  </ThemeProvider>
618
- {!shouldApplyAndroidWorkaround &&
619
- isMessageWithStylesReadByAndDateSeparator(message) &&
620
- message.dateSeparator && <InlineDateSeparator date={message.dateSeparator} />}
661
+ {!shouldApplyAndroidWorkaround && renderDateSeperator}
621
662
  {/* Adding indicator below the messages, since the list is inverted */}
622
663
  {insertInlineUnreadIndicator && <InlineUnreadIndicator />}
623
664
  </>
624
665
  ) : (
625
666
  <>
626
667
  <View testID={`message-list-item-${index}`}>
627
- {shouldApplyAndroidWorkaround &&
628
- isMessageWithStylesReadByAndDateSeparator(message) &&
629
- message.dateSeparator && <InlineDateSeparator date={message.dateSeparator} />}
630
- <Message
631
- goToMessage={goToMessage}
632
- groupStyles={
633
- isMessageWithStylesReadByAndDateSeparator(message) ? message.groupStyles : []
634
- }
635
- isTargetedMessage={targetedMessage === message.id}
636
- lastReceivedId={
637
- lastReceivedId === message.id || message.quoted_message_id
638
- ? lastReceivedId
639
- : undefined
640
- }
641
- message={message}
642
- onThreadSelect={onThreadSelect}
643
- showUnreadUnderlay={showUnreadUnderlay}
644
- style={[{ paddingHorizontal: screenPadding }, messageContainer]}
645
- threadList={threadList}
646
- />
668
+ {shouldApplyAndroidWorkaround && renderDateSeperator}
669
+ {renderMessage}
647
670
  </View>
648
- {!shouldApplyAndroidWorkaround &&
649
- isMessageWithStylesReadByAndDateSeparator(message) &&
650
- message.dateSeparator && <InlineDateSeparator date={message.dateSeparator} />}
671
+ {!shouldApplyAndroidWorkaround && renderDateSeperator}
651
672
  {/* Adding indicator below the messages, since the list is inverted */}
652
673
  {insertInlineUnreadIndicator && <InlineUnreadIndicator />}
653
674
  </>
@@ -679,12 +700,15 @@ const MessageListWithContext = <
679
700
  */
680
701
  const maybeCallOnStartReached = async (limit?: number) => {
681
702
  // If onStartReached has already been called for given data length, then ignore.
682
- if (messageList?.length && onStartReachedTracker.current[messageList.length]) {
703
+ if (
704
+ processedMessageList?.length &&
705
+ onStartReachedTracker.current[processedMessageList.length]
706
+ ) {
683
707
  return;
684
708
  }
685
709
 
686
- if (messageList?.length) {
687
- onStartReachedTracker.current[messageList.length] = true;
710
+ if (processedMessageList?.length) {
711
+ onStartReachedTracker.current[processedMessageList.length] = true;
688
712
  }
689
713
 
690
714
  const callback = () => {
@@ -716,12 +740,12 @@ const MessageListWithContext = <
716
740
  */
717
741
  const maybeCallOnEndReached = async () => {
718
742
  // If onEndReached has already been called for given messageList length, then ignore.
719
- if (messageList?.length && onEndReachedTracker.current[messageList.length]) {
743
+ if (processedMessageList?.length && onEndReachedTracker.current[processedMessageList.length]) {
720
744
  return;
721
745
  }
722
746
 
723
- if (messageList?.length) {
724
- onEndReachedTracker.current[messageList.length] = true;
747
+ if (processedMessageList?.length) {
748
+ onEndReachedTracker.current[processedMessageList.length] = true;
725
749
  }
726
750
 
727
751
  const callback = () => {
@@ -749,21 +773,16 @@ const MessageListWithContext = <
749
773
  }
750
774
  };
751
775
 
752
- /**
753
- * Method used only inside the List to do these things
754
- * 1. Mark channel as read if scroll is at the bottom
755
- * 2. Call maybeCallOnStartReached if scroll is at the top
756
- * 3. Call maybeCallOnEndReached if scroll is at the bottom
757
- * 4. Show scrollToBottom button if scroll is at the bottom and messages are not the latest
758
- */
759
- const onScrollEvent: ScrollViewProps['onScroll'] = (event) => {
776
+ const onUserScrollEvent: NonNullable<ScrollViewProps['onScroll']> = (event) => {
777
+ const nativeEvent = event.nativeEvent;
778
+ clearTimeout(onScrollEventTimeoutRef.current);
779
+ const offset = nativeEvent.contentOffset.y;
780
+ const visibleLength = nativeEvent.layoutMeasurement.height;
781
+ const contentLength = nativeEvent.contentSize.height;
760
782
  if (!channel || !channelResyncScrollSet.current) {
761
783
  return;
762
784
  }
763
785
 
764
- const offset = event.nativeEvent.contentOffset.y;
765
- const visibleLength = event.nativeEvent.layoutMeasurement.height;
766
- const contentLength = event.nativeEvent.contentSize.height;
767
786
  // Check if scroll has reached either start of end of list.
768
787
  const isScrollAtStart = offset < 100;
769
788
  const isScrollAtEnd = contentLength - visibleLength - offset < 100;
@@ -775,32 +794,38 @@ const MessageListWithContext = <
775
794
  if (isScrollAtEnd) {
776
795
  maybeCallOnEndReached();
777
796
  }
797
+ };
778
798
 
799
+ const handleScroll: ScrollViewProps['onScroll'] = (event) => {
800
+ const offset = event.nativeEvent.contentOffset.y;
779
801
  // Show scrollToBottom button once scroll position goes beyond 150.
780
802
  const isScrollAtBottom = offset <= 150;
781
803
 
804
+ const notLatestSet = channel.state.messages !== channel.state.latestMessages;
805
+
806
+ const showScrollToBottomButton =
807
+ (!threadList && notLatestSet) || !isScrollAtBottom || !hasNoMoreRecentMessagesToLoad;
808
+
782
809
  /**
783
- * Following if condition covers following cases:
784
810
  * 1. If I scroll up -> show scrollToBottom button.
785
811
  * 2. If I scroll to bottom of screen
786
812
  * |-> hide scrollToBottom button.
787
813
  * |-> if channel is unread, call markRead().
788
814
  */
789
-
790
- const showScrollToBottomButton = !isScrollAtBottom || !hasNoMoreRecentMessagesToLoad;
815
+ setScrollToBottomButtonVisible(showScrollToBottomButton);
791
816
 
792
817
  const shouldMarkRead =
793
- !threadList && offset <= 0 && hasNoMoreRecentMessagesToLoad && channel.countUnread() > 0;
818
+ !threadList &&
819
+ !notLatestSet &&
820
+ offset <= 0 &&
821
+ hasNoMoreRecentMessagesToLoad &&
822
+ channel.countUnread() > 0;
794
823
 
795
824
  if (shouldMarkRead) {
796
825
  markRead();
797
826
  }
798
827
 
799
- setScrollToBottomButtonVisible(showScrollToBottomButton);
800
- };
801
-
802
- const handleScroll: ScrollViewProps['onScroll'] = (event) => {
803
- onScrollEvent(event);
828
+ setInitialScrollDone(false);
804
829
 
805
830
  if (onListScroll) {
806
831
  onListScroll(event);
@@ -808,7 +833,10 @@ const MessageListWithContext = <
808
833
  };
809
834
 
810
835
  const goToNewMessages = async () => {
811
- if (!hasNoMoreRecentMessagesToLoad) {
836
+ const isNotLatestSet = channel.state.messages !== channel.state.latestMessages;
837
+ if (isNotLatestSet && hasNoMoreRecentMessagesToLoad) {
838
+ loadChannelAroundMessage({});
839
+ } else if (!hasNoMoreRecentMessagesToLoad) {
812
840
  resetPaginationTrackersRef.current();
813
841
  await reloadChannel();
814
842
  } else if (flatListRef.current) {
@@ -830,10 +858,6 @@ const MessageListWithContext = <
830
858
  >((info) => {
831
859
  // We got a failure as we tried to scroll to an item that was outside the render length
832
860
  if (!flatListRef.current) return;
833
- const dataLength = flatListRef.current.props?.data?.length;
834
- if (dataLength) {
835
- tempDisablePaginationTrackersRef.current(dataLength);
836
- }
837
861
  // we don't know the actual size of all items but we can see the average, so scroll to the closest offset
838
862
  flatListRef.current.scrollToOffset({
839
863
  animated: false,
@@ -843,17 +867,14 @@ const MessageListWithContext = <
843
867
  // with a little delay to wait for scroll to offset to complete, we can then scroll to the index
844
868
  failScrollTimeoutId.current = setTimeout(() => {
845
869
  try {
846
- if (dataLength) {
847
- tempDisablePaginationTrackersRef.current(dataLength);
848
- }
849
870
  flatListRef.current?.scrollToIndex({
850
871
  animated: false,
851
872
  index: info.index,
852
873
  viewPosition: 0.5, // try to place message in the center of the screen
853
874
  });
854
- // in case the target message was cleared out
855
- // the state being set again will trigger the highlight again
856
875
  if (messageIdLastScrolledToRef.current) {
876
+ // in case the target message was cleared out
877
+ // the state being set again will trigger the highlight again
857
878
  setTargetedMessage(messageIdLastScrolledToRef.current);
858
879
  }
859
880
  scrollToIndexFailedRetryCountRef.current = 0;
@@ -862,10 +883,6 @@ const MessageListWithContext = <
862
883
  !onScrollToIndexFailedRef.current ||
863
884
  scrollToIndexFailedRetryCountRef.current > MAX_RETRIES_AFTER_SCROLL_FAILURE
864
885
  ) {
865
- console.log(
866
- `Scrolling to index failed after ${MAX_RETRIES_AFTER_SCROLL_FAILURE} retries`,
867
- e,
868
- );
869
886
  scrollToIndexFailedRetryCountRef.current = 0;
870
887
  return;
871
888
  }
@@ -882,34 +899,27 @@ const MessageListWithContext = <
882
899
  // this onScrollToIndexFailed will be called again
883
900
  });
884
901
 
885
- const goToMessage = useCallback(
886
- (messageId: string) => {
887
- const indexOfParentInMessageList = messageList.findIndex(
888
- (message) => message?.id === messageId,
889
- );
890
- if (indexOfParentInMessageList !== -1 && flatListRef.current) {
891
- clearTimeout(failScrollTimeoutId.current);
892
- scrollToIndexFailedRetryCountRef.current = 0;
893
- // we are scrolling automatically to the message instead of user initiating it,
894
- // so we don't need to load more older messages
895
- tempDisablePaginationTrackersRef.current(messageList.length);
896
- // now scroll to it
897
- flatListRef.current.scrollToIndex({
898
- animated: true,
899
- index: indexOfParentInMessageList,
900
- viewPosition: 0.5, // try to place message in the center of the screen
901
- });
902
- // keep track of this messageId, so that we dont scroll to again in useEffect for targeted message change
903
- messageIdLastScrolledToRef.current = messageId;
904
- setTargetedMessage(messageId);
905
- return;
906
- }
907
- messageIdToScrollToRef.current = messageId; // keep track of the id to scroll afterwards
908
- loadChannelAroundMessage({ messageId }); // now try to load the message and whats around it
909
- resetPaginationTrackersRef.current();
910
- },
911
- [messageList],
912
- );
902
+ const goToMessage = (messageId: string) => {
903
+ const indexOfParentInMessageList = processedMessageList.findIndex(
904
+ (message) => message?.id === messageId,
905
+ );
906
+ if (indexOfParentInMessageList !== -1 && flatListRef.current) {
907
+ clearTimeout(failScrollTimeoutId.current);
908
+ scrollToIndexFailedRetryCountRef.current = 0;
909
+ // keep track of this messageId, so that we dont scroll to again in useEffect for targeted message change
910
+ messageIdLastScrolledToRef.current = messageId;
911
+ setTargetedMessage(messageId);
912
+ // now scroll to it with animated=true (in useEffect animated=false is used)
913
+ flatListRef.current.scrollToIndex({
914
+ animated: true,
915
+ index: indexOfParentInMessageList,
916
+ viewPosition: 0.5, // try to place message in the center of the screen
917
+ });
918
+ return;
919
+ }
920
+ // the message we want was not loaded yet, so lets load it
921
+ loadChannelAroundMessage({ messageId });
922
+ };
913
923
 
914
924
  /**
915
925
  * Check if a messageId needs to be scrolled to after list loads, and scroll to it
@@ -922,26 +932,24 @@ const MessageListWithContext = <
922
932
  initialScrollSettingTimeoutRef.current = setTimeout(() => {
923
933
  // small timeout to ensure that handleScroll is called after scrollToIndex to set this flag
924
934
  setInitialScrollDone(true);
925
- }, 500);
935
+ }, 2000);
926
936
  }
927
- // goToMessage method might have requested to scroll to a message
928
- let messageIdToScroll: string | undefined = messageIdToScrollToRef.current;
937
+ let messageIdToScroll: string | undefined;
929
938
  if (targetedMessage && messageIdLastScrolledToRef.current !== targetedMessage) {
930
939
  // if some messageId was targeted but not scrolledTo yet
931
940
  // we have scroll to there after loading completes
932
941
  messageIdToScroll = targetedMessage;
933
942
  }
934
943
  if (!messageIdToScroll) return;
935
- const indexOfParentInMessageList = messageList.findIndex(
944
+ const indexOfParentInMessageList = processedMessageList.findIndex(
936
945
  (message) => message?.id === messageIdToScroll,
937
946
  );
938
947
  if (indexOfParentInMessageList !== -1 && flatListRef.current) {
939
948
  // By a fresh scroll we should clear the retries for the previous failed scroll
940
949
  clearTimeout(scrollToDebounceTimeoutRef.current);
941
950
  clearTimeout(failScrollTimeoutId.current);
942
- // we are scrolling automatically to the message instead of user initiating it,
943
- // so we don't need to load more older messages
944
- tempDisablePaginationTrackersRef.current(messageList.length);
951
+ // keep track of this messageId, so that we dont scroll to again for targeted message change
952
+ messageIdLastScrolledToRef.current = messageIdToScroll;
945
953
  // reset the retry count
946
954
  scrollToIndexFailedRetryCountRef.current = 0;
947
955
  // now scroll to it
@@ -950,17 +958,13 @@ const MessageListWithContext = <
950
958
  index: indexOfParentInMessageList,
951
959
  viewPosition: 0.5, // try to place message in the center of the screen
952
960
  });
953
- // reset the messageId tracker to not scroll to that again
954
- messageIdToScrollToRef.current = undefined;
955
- // keep track of this messageId, so that we dont scroll to again for targeted message change
956
- messageIdLastScrolledToRef.current = messageIdToScroll;
957
961
  }
958
- }, 150);
959
- }, [targetedMessage, initialScrollToFirstUnreadMessage, messageList]);
962
+ }, 50);
963
+ }, [targetedMessage, initialScrollToFirstUnreadMessage]);
960
964
 
961
965
  const messagesWithImages =
962
966
  legacyImageViewerSwipeBehaviour &&
963
- messageList.filter((message) => {
967
+ processedMessageList.filter((message) => {
964
968
  const isMessageTypeDeleted = message.type === 'deleted';
965
969
  if (!isMessageTypeDeleted && message.attachments) {
966
970
  return message.attachments.some(
@@ -1030,11 +1034,11 @@ const MessageListWithContext = <
1030
1034
  };
1031
1035
  const onScrollBeginDrag: ScrollViewProps['onScrollBeginDrag'] = (event) => {
1032
1036
  !hasMoved && selectedPicker && setHasMoved(true);
1033
- onScrollEvent(event);
1037
+ onUserScrollEvent(event);
1034
1038
  };
1035
1039
  const onScrollEndDrag: ScrollViewProps['onScrollEndDrag'] = (event) => {
1036
1040
  hasMoved && selectedPicker && setHasMoved(false);
1037
- onScrollEvent(event);
1041
+ onUserScrollEvent(event);
1038
1042
  };
1039
1043
 
1040
1044
  const refCallback = (ref: FlatListType<MessageType<StreamChatGenerics>>) => {
@@ -1054,7 +1058,7 @@ const MessageListWithContext = <
1054
1058
  if (debugRef.current.setSendEventParams)
1055
1059
  debugRef.current.setSendEventParams({
1056
1060
  action: thread ? 'ThreadList' : 'Messages',
1057
- data: messageList,
1061
+ data: processedMessageList,
1058
1062
  });
1059
1063
  }
1060
1064
 
@@ -1130,20 +1134,26 @@ const MessageListWithContext = <
1130
1134
  additionalFlatListProps?.contentContainerStyle,
1131
1135
  contentContainer,
1132
1136
  ]}
1133
- data={messageList}
1134
1137
  /** Disables the MessageList UI. Which means, message actions, reactions won't work. */
1138
+ data={processedMessageList}
1135
1139
  extraData={disabled || !hasNoMoreRecentMessagesToLoad}
1136
1140
  inverted={shouldApplyAndroidWorkaround ? false : inverted}
1137
1141
  keyboardShouldPersistTaps='handled'
1138
1142
  keyExtractor={keyExtractor}
1139
1143
  ListEmptyComponent={renderListEmptyComponent}
1140
1144
  ListFooterComponent={ListFooterComponent}
1145
+ /**
1146
+ if autoscrollToTopThreshold is 10, we scroll to recent if before new list update it was already at the bottom (10 offset or below)
1147
+ minIndexForVisible = 1 means that beyond item at index 1 will not change position on list updates
1148
+ minIndexForVisible is not used when autoscrollToTopThreshold = 10
1149
+ */
1141
1150
  ListHeaderComponent={ListHeaderComponent}
1142
1151
  maintainVisibleContentPosition={{
1143
- autoscrollToTopThreshold: autoscrollToTop ? 10 : undefined,
1152
+ autoscrollToTopThreshold: autoscrollToRecent ? 10 : undefined,
1144
1153
  minIndexForVisible: 1,
1145
1154
  }}
1146
1155
  maxToRenderPerBatch={30}
1156
+ onMomentumScrollEnd={onUserScrollEvent}
1147
1157
  onScroll={handleScroll}
1148
1158
  onScrollBeginDrag={onScrollBeginDrag}
1149
1159
  onScrollEndDrag={onScrollEndDrag}
@@ -1,7 +1,7 @@
1
1
  import React, { FC } from 'react';
2
2
 
3
3
  import { renderHook } from '@testing-library/react-hooks';
4
- import type { DefaultStreamChatGenerics } from 'src/types/types';
4
+
5
5
  import type { DefaultGenerics, StreamChat } from 'stream-chat';
6
6
 
7
7
  import { useCreatePaginatedMessageListContext } from '../../../components/Channel/hooks/useCreatePaginatedMessageListContext';
@@ -15,6 +15,7 @@ import {
15
15
  import { generateMessage } from '../../../mock-builders/generator/message';
16
16
  import { generateUser } from '../../../mock-builders/generator/user';
17
17
  import { getTestClientWithUser } from '../../../mock-builders/mock';
18
+ import type { DefaultStreamChatGenerics } from '../../../types/types';
18
19
  import { useMessageList } from '../hooks/useMessageList';
19
20
 
20
21
  const clientUser = generateUser();
@@ -60,6 +61,8 @@ describe('useMessageList', () => {
60
61
  { wrapper: Providers },
61
62
  );
62
63
  const reversedMessages = messages.reverse();
63
- expect(result.current.map(({ id }) => id)).toEqual(reversedMessages.map(({ id }) => id));
64
+ expect(result.current.processedMessageList.map(({ id }) => id)).toEqual(
65
+ reversedMessages.map(({ id }) => id),
66
+ );
64
67
  });
65
68
  });
@@ -103,7 +103,14 @@ export const useMessageList = <
103
103
  readBy: msg.id ? readData[msg.id] || false : false,
104
104
  }));
105
105
 
106
- return [
106
+ const processedMessageList = [
107
107
  ...messagesWithStylesReadByAndDateSeparator,
108
108
  ].reverse() as MessageType<StreamChatGenerics>[];
109
+ const rawMessageList = messageList;
110
+ return {
111
+ /** Messages enriched with dates/readby/groups and also reversed in order */
112
+ processedMessageList,
113
+ /** Raw messages from the channel state */
114
+ rawMessageList,
115
+ };
109
116
  };