stream-chat-react-native-core 5.12.0-beta.2 → 5.12.0-beta.4

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 (35) hide show
  1. package/lib/commonjs/components/Channel/Channel.js +40 -24
  2. package/lib/commonjs/components/Channel/Channel.js.map +1 -1
  3. package/lib/commonjs/components/Channel/hooks/useTargetedMessage.js +3 -3
  4. package/lib/commonjs/components/Channel/hooks/useTargetedMessage.js.map +1 -1
  5. package/lib/commonjs/components/ChannelPreview/ChannelPreview.js +9 -2
  6. package/lib/commonjs/components/ChannelPreview/ChannelPreview.js.map +1 -1
  7. package/lib/commonjs/components/MessageList/MessageList.js +96 -80
  8. package/lib/commonjs/components/MessageList/MessageList.js.map +1 -1
  9. package/lib/commonjs/components/MessageList/utils/getReadStates.js +9 -4
  10. package/lib/commonjs/components/MessageList/utils/getReadStates.js.map +1 -1
  11. package/lib/commonjs/hooks/useAppStateListener.js +14 -10
  12. package/lib/commonjs/hooks/useAppStateListener.js.map +1 -1
  13. package/lib/commonjs/version.json +1 -1
  14. package/lib/module/components/Channel/Channel.js +40 -24
  15. package/lib/module/components/Channel/Channel.js.map +1 -1
  16. package/lib/module/components/Channel/hooks/useTargetedMessage.js +3 -3
  17. package/lib/module/components/Channel/hooks/useTargetedMessage.js.map +1 -1
  18. package/lib/module/components/ChannelPreview/ChannelPreview.js +9 -2
  19. package/lib/module/components/ChannelPreview/ChannelPreview.js.map +1 -1
  20. package/lib/module/components/MessageList/MessageList.js +96 -80
  21. package/lib/module/components/MessageList/MessageList.js.map +1 -1
  22. package/lib/module/components/MessageList/utils/getReadStates.js +9 -4
  23. package/lib/module/components/MessageList/utils/getReadStates.js.map +1 -1
  24. package/lib/module/hooks/useAppStateListener.js +14 -10
  25. package/lib/module/hooks/useAppStateListener.js.map +1 -1
  26. package/lib/module/version.json +1 -1
  27. package/package.json +1 -1
  28. package/src/components/Channel/Channel.tsx +35 -15
  29. package/src/components/Channel/hooks/useTargetedMessage.ts +3 -3
  30. package/src/components/ChannelPreview/ChannelPreview.tsx +7 -0
  31. package/src/components/MessageList/MessageList.tsx +75 -58
  32. package/src/components/MessageList/utils/getReadStates.ts +3 -4
  33. package/src/components/Thread/__tests__/__snapshots__/Thread.test.js.snap +2 -0
  34. package/src/hooks/useAppStateListener.ts +14 -11
  35. package/src/version.json +1 -1
@@ -833,22 +833,42 @@ const ChannelWithContext = <
833
833
  */
834
834
  const loadChannelAtFirstUnreadMessage = () => {
835
835
  if (!channel) return;
836
- const unreadCount = channel.countUnread();
837
- if (unreadCount <= scrollToFirstUnreadThreshold) return;
838
- // temporarily clear existing messages so that messageList component gets a list change and does not scroll to any unread message first before loading completes
839
- setMessages([]);
836
+ let unreadMessageIdToScrollTo: string | undefined;
840
837
  // query for messages around the last read date
841
- return channelQueryCallRef.current(async () => {
842
- setLoading(true);
843
- const lastReadDate = channel.lastRead() || new Date(0);
844
- await channel.query({
845
- messages: {
846
- created_at_around: lastReadDate,
847
- limit: 25,
848
- },
849
- });
850
- setLoading(false);
851
- });
838
+ return channelQueryCallRef.current(
839
+ async () => {
840
+ setLoading(true);
841
+ const lastReadDate = channel.lastRead();
842
+ // if last read date is present we can just fetch messages around that date
843
+ // last read date not being present is an edge case if somewhere the user of SDK deletes the read state (this will usually never happen)
844
+ if (lastReadDate) {
845
+ setHasNoMoreRecentMessagesToLoad(false); // we are jumping to a message, hence we do not know for sure anymore if there are no more recent messages
846
+ // get totally 30 messages... max 15 before last read date and max 15 after last read date
847
+ // ref: https://github.com/GetStream/chat/pull/2588
848
+ await channel.query(
849
+ {
850
+ messages: {
851
+ created_at_around: lastReadDate,
852
+ limit: 30,
853
+ },
854
+ },
855
+ 'new',
856
+ );
857
+ unreadMessageIdToScrollTo = channel.state.messages.find(
858
+ (m) => lastReadDate < m.created_at,
859
+ )?.id;
860
+ } else {
861
+ // we just load the latest messages (25 is the default) and we cant scroll to first unread message
862
+ await channel.state.loadMessageIntoState('latest');
863
+ }
864
+ setLoading(false);
865
+ },
866
+ () => {
867
+ if (unreadMessageIdToScrollTo) {
868
+ setTargetedMessage(unreadMessageIdToScrollTo);
869
+ }
870
+ },
871
+ );
852
872
  };
853
873
 
854
874
  /**
@@ -14,7 +14,7 @@ export const useTargetedMessage = (messageId?: string) => {
14
14
  };
15
15
  }, []);
16
16
 
17
- const setTargetedMessageTimeout = (messageId: string) => {
17
+ const setTargetedMessageTimeoutRef = useRef((messageId: string) => {
18
18
  clearTargetedMessageCall.current && clearTimeout(clearTargetedMessageCall.current);
19
19
 
20
20
  clearTargetedMessageCall.current = setTimeout(() => {
@@ -22,10 +22,10 @@ export const useTargetedMessage = (messageId?: string) => {
22
22
  }, 3000);
23
23
 
24
24
  setTargetedMessage(messageId);
25
- };
25
+ });
26
26
 
27
27
  return {
28
- setTargetedMessage: setTargetedMessageTimeout,
28
+ setTargetedMessage: setTargetedMessageTimeoutRef.current,
29
29
  targetedMessage,
30
30
  };
31
31
  };
@@ -52,6 +52,13 @@ const ChannelPreviewWithContext = <
52
52
  const channelLastMessage = channel.lastMessage();
53
53
  const channelLastMessageString = `${channelLastMessage?.id}${channelLastMessage?.updated_at}`;
54
54
 
55
+ useEffect(() => {
56
+ const { unsubscribe } = client.on('notification.mark_read', () => {
57
+ setUnread(channel.countUnread());
58
+ });
59
+ return unsubscribe;
60
+ }, []);
61
+
55
62
  useEffect(() => {
56
63
  if (
57
64
  channelLastMessage &&
@@ -272,7 +272,6 @@ const MessageListWithContext = <
272
272
  overlay,
273
273
  reloadChannel,
274
274
  ScrollToBottomButton,
275
- scrollToFirstUnreadThreshold,
276
275
  selectedPicker,
277
276
  setFlatListRef,
278
277
  setMessages,
@@ -332,8 +331,7 @@ const MessageListWithContext = <
332
331
  * If the prop `initialScrollToFirstUnreadMessage` was enabled, then we scroll to the unread msg and set it to true
333
332
  * If not, the default offset of 0 for flatList means that it has been set already
334
333
  */
335
- const initialScrollSet = useRef<boolean>(!initialScrollToFirstUnreadMessage);
336
-
334
+ const [isInitialScrollDone, setInitialScrollDone] = useState(!initialScrollToFirstUnreadMessage);
337
335
  const channelResyncScrollSet = useRef<boolean>(true);
338
336
 
339
337
  /**
@@ -341,6 +339,11 @@ const MessageListWithContext = <
341
339
  */
342
340
  const scrollToDebounceTimeoutRef = useRef<NodeJS.Timeout>();
343
341
 
342
+ /**
343
+ * The timeout id used to lazier load the initial scroll set flag
344
+ */
345
+ const initialScrollSettingTimeoutRef = useRef<NodeJS.Timeout>();
346
+
344
347
  /**
345
348
  * If a messageId was requested to scroll to but was unloaded,
346
349
  * this flag keeps track of it to scroll to it after loading the message
@@ -423,29 +426,39 @@ const MessageListWithContext = <
423
426
  }, [disabled]);
424
427
 
425
428
  useEffect(() => {
426
- /**
427
- * 1. !initialScrollToFirstUnreadMessage && channel.countUnread() > 0
428
- *
429
- * In this case MessageList won't scroll to first unread message when opened, so we can mark
430
- * the channel as read right after opening.
431
- *
432
- * 2. initialScrollToFirstUnreadMessage && channel.countUnread() <= scrollToFirstUnreadThreshold
433
- *
434
- * In this case MessageList will be opened to first unread message.
435
- * But if there are not enough (scrollToFirstUnreadThreshold) unread messages, then MessageList
436
- * won't need to scroll up. So we can safely mark the channel as read right after opening.
437
- */
438
- const shouldMarkReadOnFirstLoad =
439
- !loading &&
440
- channel &&
441
- ((!initialScrollToFirstUnreadMessage && channel.countUnread() > 0) ||
442
- (initialScrollToFirstUnreadMessage &&
443
- channel.countUnread() <= scrollToFirstUnreadThreshold));
444
-
445
- if (shouldMarkReadOnFirstLoad) {
429
+ const getShouldMarkReadAutomatically = (): boolean => {
430
+ if (loading || !channel) {
431
+ // nothing to do
432
+ return false;
433
+ } else if (channel.countUnread() > 0) {
434
+ if (!initialScrollToFirstUnreadMessage) {
435
+ /*
436
+ * In this case MessageList won't scroll to first unread message when opened, so we can mark
437
+ * the channel as read right after opening.
438
+ * */
439
+ return true;
440
+ } else {
441
+ /*
442
+ * In this case MessageList will be opened to first unread message.
443
+ * But if there are were not enough unread messages, so that scrollToBottom button was not shown
444
+ * then MessageList won't need to scroll up. So we can safely mark the channel as read right after opening.
445
+ *
446
+ * NOTE: we must ensure that initial scroll is done, otherwise we do not wait till the unread scroll is finished
447
+ * */
448
+ if (scrollToBottomButtonVisible) return false;
449
+ /* if scrollToBottom button was not visible, wait till
450
+ * - initial scroll is done (indicates that if scrolling to index was needed it was triggered)
451
+ * */
452
+ return isInitialScrollDone;
453
+ }
454
+ }
455
+ return false;
456
+ };
457
+
458
+ if (getShouldMarkReadAutomatically()) {
446
459
  markRead();
447
460
  }
448
- }, [loading]);
461
+ }, [loading, scrollToBottomButtonVisible, isInitialScrollDone]);
449
462
 
450
463
  useEffect(() => {
451
464
  const lastReceivedMessage = getLastReceivedMessage(messageList);
@@ -492,7 +505,7 @@ const MessageListWithContext = <
492
505
 
493
506
  if (threadList || hasNoMoreRecentMessagesToLoad) {
494
507
  scrollToBottomIfNeeded();
495
- } else if (!scrollToBottomButtonVisible) {
508
+ } else {
496
509
  setScrollToBottomButtonVisible(true);
497
510
  }
498
511
 
@@ -529,23 +542,17 @@ const MessageListWithContext = <
529
542
  if (!channel || (!channel.initialized && !channel.offlineMode)) return null;
530
543
 
531
544
  const lastRead = channel.lastRead();
532
- const countUnread = channel.countUnread();
533
545
 
534
546
  function isMessageUnread(messageArrayIndex: number): boolean {
535
- if (lastRead && message.created_at) {
536
- return lastRead < message.created_at;
537
- } else {
538
- const isLatestMessageSetShown = !!channel.state.messageSets.find(
539
- (set) => set.isCurrent && set.isLatest,
540
- );
541
- return isLatestMessageSetShown && messageArrayIndex <= countUnread - 1;
547
+ const msg = messageList?.[messageArrayIndex];
548
+ if (lastRead && msg?.created_at) {
549
+ return lastRead < msg.created_at;
542
550
  }
551
+ return false;
543
552
  }
544
553
  const isCurrentMessageUnread = isMessageUnread(index);
545
- const isLastMessageUnread = isMessageUnread(index + 1);
546
-
547
554
  const showUnreadUnderlay = isCurrentMessageUnread && scrollToBottomButtonVisible;
548
- const insertInlineUnreadIndicator = showUnreadUnderlay && !isLastMessageUnread;
555
+ const insertInlineUnreadIndicator = showUnreadUnderlay && !isMessageUnread(index + 1); // show only if previous message is read
549
556
 
550
557
  if (message.type === 'system') {
551
558
  return (
@@ -742,8 +749,8 @@ const MessageListWithContext = <
742
749
  maybeCallOnEndReached();
743
750
  }
744
751
 
745
- // Show scrollToBottom button once scroll position goes beyond 300.
746
- const isScrollAtBottom = offset <= 300;
752
+ // Show scrollToBottom button once scroll position goes beyond 150.
753
+ const isScrollAtBottom = offset <= 150;
747
754
  const showScrollToBottomButton = !isScrollAtBottom || !hasNoMoreRecentMessagesToLoad;
748
755
 
749
756
  const shouldMarkRead =
@@ -829,21 +836,16 @@ const MessageListWithContext = <
829
836
  * Note: This effect fires on every list change with a small debounce so that scrolling isnt abrupted by an immediate rerender
830
837
  */
831
838
  useEffect(() => {
832
- if (scrollToDebounceTimeoutRef.current) clearTimeout(scrollToDebounceTimeoutRef.current);
833
839
  scrollToDebounceTimeoutRef.current = setTimeout(() => {
840
+ if (initialScrollToFirstUnreadMessage) {
841
+ initialScrollSettingTimeoutRef.current = setTimeout(() => {
842
+ // small timeout to ensure that handleScroll is called after scrollToIndex to set this flag
843
+ setInitialScrollDone(true);
844
+ }, 500);
845
+ }
834
846
  // goToMessage method might have requested to scroll to a message
835
847
  let messageIdToScroll: string | undefined = messageIdToScrollToRef.current;
836
- const countUnread = channelRef.current?.countUnread();
837
- if (
838
- !initialScrollSet.current &&
839
- initialScrollToFirstUnreadMessage &&
840
- countUnread > scrollToFirstUnreadThreshold
841
- ) {
842
- // find the first unread message, if we have to initially scroll to an unread message
843
- if (messageList.length >= countUnread) {
844
- messageIdToScroll = messageList[countUnread - 1].id;
845
- }
846
- } else if (targetedMessage && messageIdLastScrolledToRef.current !== targetedMessage) {
848
+ if (targetedMessage && messageIdLastScrolledToRef.current !== targetedMessage) {
847
849
  // if some messageId was targeted but not scrolledTo yet
848
850
  // we have scroll to there after loading completes
849
851
  messageIdToScroll = targetedMessage;
@@ -862,14 +864,13 @@ const MessageListWithContext = <
862
864
  messageIdToScrollToRef.current = undefined;
863
865
  // keep track of this messageId, so that we dont scroll to again for targeted message change
864
866
  messageIdLastScrolledToRef.current = messageIdToScroll;
865
- if (!initialScrollSet.current && initialScrollToFirstUnreadMessage) {
866
- initialScrollSet.current = true;
867
- } else {
868
- setTargetedMessage(messageIdToScroll);
869
- }
870
867
  }
871
868
  }, 150);
872
- }, [channel.initialized, messageList, targetedMessage, initialScrollToFirstUnreadMessage]);
869
+ return () => {
870
+ clearTimeout(scrollToDebounceTimeoutRef.current);
871
+ clearTimeout(initialScrollSettingTimeoutRef.current);
872
+ };
873
+ }, [targetedMessage, initialScrollToFirstUnreadMessage, messageList]);
873
874
 
874
875
  const messagesWithImages =
875
876
  legacyImageViewerSwipeBehaviour &&
@@ -1012,6 +1013,17 @@ const MessageListWithContext = <
1012
1013
  return null;
1013
1014
  };
1014
1015
 
1016
+ // We need to omit the style related props from the additionalFlatListProps and add them directly instead of spreading
1017
+ let additionalFlatListPropsExcludingStyle:
1018
+ | Omit<NonNullable<typeof additionalFlatListProps>, 'style' | 'contentContainerStyle'>
1019
+ | undefined;
1020
+
1021
+ if (additionalFlatListProps) {
1022
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1023
+ const { contentContainerStyle, style, ...rest } = additionalFlatListProps;
1024
+ additionalFlatListPropsExcludingStyle = rest;
1025
+ }
1026
+
1015
1027
  return (
1016
1028
  <View
1017
1029
  style={[styles.container, { backgroundColor: white_snow }, container]}
@@ -1021,7 +1033,11 @@ const MessageListWithContext = <
1021
1033
  CellRendererComponent={
1022
1034
  shouldApplyAndroidWorkaround ? InvertedCellRendererComponent : undefined
1023
1035
  }
1024
- contentContainerStyle={[styles.contentContainer, contentContainer]}
1036
+ contentContainerStyle={[
1037
+ styles.contentContainer,
1038
+ additionalFlatListProps?.contentContainerStyle,
1039
+ contentContainer,
1040
+ ]}
1025
1041
  data={messageList}
1026
1042
  /** Disables the MessageList UI. Which means, message actions, reactions won't work. */
1027
1043
  extraData={disabled || !hasNoMoreRecentMessagesToLoad}
@@ -1049,11 +1065,12 @@ const MessageListWithContext = <
1049
1065
  style={[
1050
1066
  styles.listContainer,
1051
1067
  listContainer,
1068
+ additionalFlatListProps?.style,
1052
1069
  shouldApplyAndroidWorkaround ? styles.invertAndroid : undefined,
1053
1070
  ]}
1054
1071
  testID='message-flat-list'
1055
1072
  viewabilityConfig={flatListViewabilityConfig}
1056
- {...additionalFlatListProps}
1073
+ {...additionalFlatListPropsExcludingStyle}
1057
1074
  />
1058
1075
  {!loading && (
1059
1076
  <>
@@ -25,10 +25,9 @@ export const getReadStates = <
25
25
  /**
26
26
  * Channel read state is stored by user and we only care about users who aren't the client
27
27
  */
28
- if (clientUserId) {
29
- delete read[clientUserId];
30
- }
31
- const members = Object.values(read);
28
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
29
+ const { [clientUserId ?? '']: _ignore, ...filteredRead } = read;
30
+ const members = Object.values(filteredRead);
32
31
 
33
32
  /**
34
33
  * Track number of members who have read previous messages
@@ -43,6 +43,7 @@ exports[`Thread should match thread snapshot 1`] = `
43
43
  "flexGrow": 1,
44
44
  "paddingBottom": 4,
45
45
  },
46
+ undefined,
46
47
  Object {},
47
48
  ]
48
49
  }
@@ -180,6 +181,7 @@ exports[`Thread should match thread snapshot 1`] = `
180
181
  },
181
182
  Object {},
182
183
  undefined,
184
+ undefined,
183
185
  ],
184
186
  ]
185
187
  }
@@ -1,22 +1,25 @@
1
- import { useCallback, useEffect, useRef } from 'react';
1
+ import { useEffect, useRef } from 'react';
2
2
  import { AppState, AppStateStatus } from 'react-native';
3
3
 
4
4
  export const useAppStateListener = (onForeground?: () => void, onBackground?: () => void) => {
5
5
  const appStateRef = useRef(AppState.currentState);
6
- const handleAppStateChange = useCallback(
7
- (nextAppState: AppStateStatus) => {
6
+ const onForegroundRef = useRef(onForeground);
7
+ const onBackgroundRef = useRef(onBackground);
8
+
9
+ // setting refs to avoid passing the functions as dependencies to useEffect
10
+ onForegroundRef.current = onForeground;
11
+ onBackgroundRef.current = onBackground;
12
+
13
+ useEffect(() => {
14
+ const handleAppStateChange = (nextAppState: AppStateStatus) => {
8
15
  const prevAppState = appStateRef.current;
9
16
  if (prevAppState.match(/inactive|background/) && nextAppState === 'active') {
10
- onForeground?.();
17
+ onForegroundRef.current?.();
11
18
  } else if (prevAppState === 'active' && nextAppState.match(/inactive|background/)) {
12
- onBackground?.();
19
+ onBackgroundRef.current?.();
13
20
  }
14
21
  appStateRef.current = nextAppState;
15
- },
16
- [onBackground, onForeground],
17
- );
18
-
19
- useEffect(() => {
22
+ };
20
23
  const subscription = AppState.addEventListener('change', handleAppStateChange);
21
24
 
22
25
  return () => {
@@ -28,5 +31,5 @@ export const useAppStateListener = (onForeground?: () => void, onBackground?: ()
28
31
  AppState.removeEventListener('change', handleAppStateChange);
29
32
  }
30
33
  };
31
- }, [handleAppStateChange]);
34
+ }, []);
32
35
  };
package/src/version.json CHANGED
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "5.12.0-beta.2"
2
+ "version": "5.12.0-beta.4"
3
3
  }