stream-chat-react-native-core 5.23.0-beta.1 → 5.23.0-beta.3

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