stream-chat-react-native-core 8.5.0 → 8.5.1-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 (38) hide show
  1. package/README.md +1 -1
  2. package/lib/commonjs/components/Message/MessageSimple/utils/renderText.js +1 -1
  3. package/lib/commonjs/components/Message/MessageSimple/utils/renderText.js.map +1 -1
  4. package/lib/commonjs/components/MessageList/MessageList.js +13 -4
  5. package/lib/commonjs/components/MessageList/MessageList.js.map +1 -1
  6. package/lib/commonjs/components/MessageList/hooks/useMessageList.js +12 -7
  7. package/lib/commonjs/components/MessageList/hooks/useMessageList.js.map +1 -1
  8. package/lib/commonjs/hooks/index.js +11 -0
  9. package/lib/commonjs/hooks/index.js.map +1 -1
  10. package/lib/commonjs/hooks/useRAFCoalescedValue.js +35 -0
  11. package/lib/commonjs/hooks/useRAFCoalescedValue.js.map +1 -0
  12. package/lib/commonjs/version.json +1 -1
  13. package/lib/module/components/Message/MessageSimple/utils/renderText.js +1 -1
  14. package/lib/module/components/Message/MessageSimple/utils/renderText.js.map +1 -1
  15. package/lib/module/components/MessageList/MessageList.js +13 -4
  16. package/lib/module/components/MessageList/MessageList.js.map +1 -1
  17. package/lib/module/components/MessageList/hooks/useMessageList.js +12 -7
  18. package/lib/module/components/MessageList/hooks/useMessageList.js.map +1 -1
  19. package/lib/module/hooks/index.js +11 -0
  20. package/lib/module/hooks/index.js.map +1 -1
  21. package/lib/module/hooks/useRAFCoalescedValue.js +35 -0
  22. package/lib/module/hooks/useRAFCoalescedValue.js.map +1 -0
  23. package/lib/module/version.json +1 -1
  24. package/lib/typescript/components/MessageList/MessageList.d.ts +9 -0
  25. package/lib/typescript/components/MessageList/MessageList.d.ts.map +1 -1
  26. package/lib/typescript/components/MessageList/hooks/useMessageList.d.ts +1 -0
  27. package/lib/typescript/components/MessageList/hooks/useMessageList.d.ts.map +1 -1
  28. package/lib/typescript/hooks/index.d.ts +1 -0
  29. package/lib/typescript/hooks/index.d.ts.map +1 -1
  30. package/lib/typescript/hooks/useRAFCoalescedValue.d.ts +43 -0
  31. package/lib/typescript/hooks/useRAFCoalescedValue.d.ts.map +1 -0
  32. package/package.json +1 -1
  33. package/src/components/Message/MessageSimple/utils/renderText.tsx +1 -1
  34. package/src/components/MessageList/MessageList.tsx +27 -5
  35. package/src/components/MessageList/hooks/useMessageList.ts +18 -11
  36. package/src/hooks/index.ts +1 -0
  37. package/src/hooks/useRAFCoalescedValue.ts +76 -0
  38. package/src/version.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/hooks/index.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAC;AACtC,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC;AAC9B,cAAc,uBAAuB,CAAC;AACtC,cAAc,iBAAiB,CAAC;AAChC,cAAc,qBAAqB,CAAC;AACpC,cAAc,mBAAmB,CAAC;AAClC,cAAc,sBAAsB,CAAC;AACrC,cAAc,qBAAqB,CAAC;AACpC,cAAc,0BAA0B,CAAC;AACzC,cAAc,8BAA8B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/hooks/index.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAC;AACtC,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC;AAC9B,cAAc,uBAAuB,CAAC;AACtC,cAAc,iBAAiB,CAAC;AAChC,cAAc,qBAAqB,CAAC;AACpC,cAAc,mBAAmB,CAAC;AAClC,cAAc,sBAAsB,CAAC;AACrC,cAAc,qBAAqB,CAAC;AACpC,cAAc,0BAA0B,CAAC;AACzC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,wBAAwB,CAAC"}
@@ -0,0 +1,43 @@
1
+ /**
2
+ * A utility hook that coalesces a fast changing value to the display’s frame rate.
3
+ * It accepts any “noisy” input (arrays, objects, numbers, etc.) and exposes a value
4
+ * that React consumers will see at most once per animation frame (via
5
+ * `requestAnimationFrame`). This is useful when upstream sources (selectors, sockets,
6
+ * DB listeners) can fire multiple times within a single paint and you want to avoid
7
+ * extra renders and layout churn.
8
+ *
9
+ * How it works:
10
+ * - Keeps track of the latest incoming value
11
+ * - Ensures there is **at most one** pending RAF at a time
12
+ * - When the RAF fires, commits the **latest** value to state (`emitted`)
13
+ * - If additional changes arrive before the RAF runs, they are merged (the last write
14
+ * operation wins) and no new RAF is scheduled
15
+ *
16
+ * With this hook you can:
17
+ * - Feed a `FlatList`/`SectionList` from fast changing sources without spamming re-renders
18
+ * - Align React updates to the paint cadence (one publish per frame)
19
+ * - Help preserve item anchoring logic (e.g., MVCP) by reducing in-frame updates
20
+ *
21
+ * **Caveats:**
22
+ * - This hook intentionally skips intermediate states that occur within the same
23
+ * frame. If you must observe every transition (e.g., for analytics/reducers), do that
24
+ * upstream; this hook is for visual coalescing
25
+ * - Equality checks are simple referential equalities. If your producer recreates arrays
26
+ * or objects each time, you’ll still publish once per frame. To avoid even those
27
+ * emissions, stabilize upstream
28
+ * - This is not a silver bullet for throttle/debounce; it uses the screen’s refresh cycle;
29
+ * If you need “no more than once per X ms”, layer that upstream
30
+ *
31
+ * Usage tips:
32
+ * - Prefer passing already-memoized values when possible (e.g., stable arrays by ID).
33
+ * - Pair with a stable `keyExtractor` in lists so coalesced updates map cleanly to rows.
34
+ * - Do not cancel/reschedule on prop changes; cancellation is handled on unmount only.
35
+ *
36
+ * @param value The upstream value that may change multiple times within a single frame.
37
+ * @param isEnabled Determines whether the hook should be run or not (useful for cases where
38
+ * we want to conditionally use RAF when certain feature feature flags are enabled). If `false`,
39
+ * it will simply pass the data through (maintaining the reference as well).
40
+ * @returns A value that updates **at most once per frame** with the latest input.
41
+ */
42
+ export declare const useRAFCoalescedValue: <S>(value: S, isEnabled: boolean | undefined) => S;
43
+ //# sourceMappingURL=useRAFCoalescedValue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useRAFCoalescedValue.d.ts","sourceRoot":"","sources":["../../../src/hooks/useRAFCoalescedValue.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,eAAO,MAAM,oBAAoB,GAAI,CAAC,EAAE,OAAO,CAAC,EAAE,WAAW,OAAO,GAAG,SAAS,KAAG,CAgClF,CAAC"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "stream-chat-react-native-core",
3
3
  "description": "The official React Native and Expo components for Stream Chat, a service for building chat applications",
4
- "version": "8.5.0",
4
+ "version": "8.5.1-beta.2",
5
5
  "author": {
6
6
  "company": "Stream.io Inc",
7
7
  "name": "Stream.io Inc"
@@ -525,7 +525,7 @@ const Bullet = ({ index, style }: BulletProps) => (
525
525
  );
526
526
 
527
527
  const ListRow = ({ children, style }: PropsWithChildren<ViewProps>) => (
528
- <Text style={style}>{children}</Text>
528
+ <View style={style}>{children}</View>
529
529
  );
530
530
 
531
531
  const ListItem = ({ children, style }: PropsWithChildren<TextProps>) => (
@@ -222,6 +222,15 @@ type MessageListPropsWithContext = Pick<
222
222
  * ```
223
223
  */
224
224
  setFlatListRef?: (ref: FlatListType<LocalMessage> | null) => void;
225
+ /**
226
+ * If true, the message list will be used in a live-streaming scenario.
227
+ * This flag is used to make sure that the auto scroll behaves well, if multiple messages are received.
228
+ *
229
+ * This flag is experimental and is subject to change. Please test thoroughly before using it.
230
+ *
231
+ * @experimental
232
+ */
233
+ isLiveStreaming?: boolean;
225
234
  };
226
235
 
227
236
  /**
@@ -256,6 +265,7 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => {
256
265
  InlineUnreadIndicator,
257
266
  inverted = true,
258
267
  isListActive = false,
268
+ isLiveStreaming = false,
259
269
  legacyImageViewerSwipeBehaviour,
260
270
  loadChannelAroundMessage,
261
271
  loading,
@@ -313,6 +323,7 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => {
313
323
  */
314
324
  const { dateSeparatorsRef, messageGroupStylesRef, processedMessageList, rawMessageList } =
315
325
  useMessageList({
326
+ isLiveStreaming,
316
327
  noGroupByUser,
317
328
  threadList,
318
329
  });
@@ -336,12 +347,17 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => {
336
347
 
337
348
  const minIndexForVisible = Math.min(1, processedMessageList.length);
338
349
 
350
+ const autoscrollToTopThreshold = useMemo(
351
+ () => (isLiveStreaming ? 64 : autoscrollToRecent ? 10 : undefined),
352
+ [autoscrollToRecent, isLiveStreaming],
353
+ );
354
+
339
355
  const maintainVisibleContentPosition = useMemo(
340
356
  () => ({
341
- autoscrollToTopThreshold: autoscrollToRecent ? 10 : undefined,
357
+ autoscrollToTopThreshold,
342
358
  minIndexForVisible,
343
359
  }),
344
- [autoscrollToRecent, minIndexForVisible],
360
+ [autoscrollToTopThreshold, minIndexForVisible],
345
361
  );
346
362
 
347
363
  /**
@@ -652,7 +668,11 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => {
652
668
  latestNonCurrentMessageBeforeUpdate?.id === latestCurrentMessageAfterUpdate.id;
653
669
  // if didMergeMessageSetsWithNoUpdates=false, we got new messages
654
670
  // so we should scroll to bottom if we are near the bottom already
655
- setAutoscrollToRecent(!didMergeMessageSetsWithNoUpdates);
671
+ const shouldForceScrollToRecent =
672
+ !didMergeMessageSetsWithNoUpdates ||
673
+ processedMessageList.length - messageListLengthBeforeUpdate.current > 0;
674
+
675
+ setAutoscrollToRecent(shouldForceScrollToRecent);
656
676
 
657
677
  if (!didMergeMessageSetsWithNoUpdates) {
658
678
  const shouldScrollToRecentOnNewOwnMessage = shouldScrollToRecentOnNewOwnMessageRef.current();
@@ -667,8 +687,7 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => {
667
687
  }, WAIT_FOR_SCROLL_TIMEOUT); // flatlist might take a bit to update, so a small delay is needed
668
688
  }
669
689
  }
670
- // eslint-disable-next-line react-hooks/exhaustive-deps
671
- }, [channel, processedMessageList, threadList]);
690
+ }, [channel, threadList, processedMessageList, shouldScrollToRecentOnNewOwnMessageRef]);
672
691
 
673
692
  const goToMessage = useStableCallback(async (messageId: string) => {
674
693
  const indexOfParentInMessageList = processedMessageList.findIndex(
@@ -1218,7 +1237,10 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => {
1218
1237
  onViewableItemsChanged={stableOnViewableItemsChanged}
1219
1238
  ref={refCallback}
1220
1239
  renderItem={renderItem}
1240
+ scrollEventThrottle={isLiveStreaming ? 16 : undefined}
1221
1241
  showsVerticalScrollIndicator={false}
1242
+ // @ts-expect-error react-native internal
1243
+ strictMode={isLiveStreaming}
1222
1244
  style={flatListStyle}
1223
1245
  testID='message-flat-list'
1224
1246
  viewabilityConfig={flatListViewabilityConfig}
@@ -11,6 +11,7 @@ import {
11
11
  import { usePaginatedMessageListContext } from '../../../contexts/paginatedMessageListContext/PaginatedMessageListContext';
12
12
  import { useThreadContext } from '../../../contexts/threadContext/ThreadContext';
13
13
 
14
+ import { useRAFCoalescedValue } from '../../../hooks';
14
15
  import { DateSeparators, getDateSeparators } from '../utils/getDateSeparators';
15
16
  import { getGroupStyles } from '../utils/getGroupStyles';
16
17
 
@@ -18,6 +19,7 @@ export type UseMessageListParams = {
18
19
  deletedMessagesVisibilityType?: DeletedMessagesVisibilityType;
19
20
  noGroupByUser?: boolean;
20
21
  threadList?: boolean;
22
+ isLiveStreaming?: boolean;
21
23
  };
22
24
 
23
25
  export type GroupType = string;
@@ -48,7 +50,7 @@ export const shouldIncludeMessageInList = (
48
50
  };
49
51
 
50
52
  export const useMessageList = (params: UseMessageListParams) => {
51
- const { noGroupByUser, threadList } = params;
53
+ const { noGroupByUser, threadList, isLiveStreaming } = params;
52
54
  const { client } = useChatContext();
53
55
  const { hideDateSeparators, maxTimeBetweenGroupedMessages } = useChannelContext();
54
56
  const { deletedMessagesVisibilityType, getMessagesGroupStyles = getGroupStyles } =
@@ -110,14 +112,19 @@ export const useMessageList = (params: UseMessageListParams) => {
110
112
  return newMessageList;
111
113
  }, [client.userID, deletedMessagesVisibilityType, messageList]);
112
114
 
113
- return {
114
- /** Date separators */
115
- dateSeparatorsRef,
116
- /** Message group styles */
117
- messageGroupStylesRef,
118
- /** Messages enriched with dates/readby/groups and also reversed in order */
119
- processedMessageList,
120
- /** Raw messages from the channel state */
121
- rawMessageList: messageList,
122
- };
115
+ const data = useRAFCoalescedValue(processedMessageList, isLiveStreaming);
116
+
117
+ return useMemo(
118
+ () => ({
119
+ /** Date separators */
120
+ dateSeparatorsRef,
121
+ /** Message group styles */
122
+ messageGroupStylesRef,
123
+ /** Messages enriched with dates/readby/groups and also reversed in order */
124
+ processedMessageList: data,
125
+ /** Raw messages from the channel state */
126
+ rawMessageList: messageList,
127
+ }),
128
+ [data, messageList],
129
+ );
123
130
  };
@@ -9,3 +9,4 @@ export * from './useMessageReminder';
9
9
  export * from './useQueryReminders';
10
10
  export * from './useClientNotifications';
11
11
  export * from './useInAppNotificationsState';
12
+ export * from './useRAFCoalescedValue';
@@ -0,0 +1,76 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+
3
+ /**
4
+ * A utility hook that coalesces a fast changing value to the display’s frame rate.
5
+ * It accepts any “noisy” input (arrays, objects, numbers, etc.) and exposes a value
6
+ * that React consumers will see at most once per animation frame (via
7
+ * `requestAnimationFrame`). This is useful when upstream sources (selectors, sockets,
8
+ * DB listeners) can fire multiple times within a single paint and you want to avoid
9
+ * extra renders and layout churn.
10
+ *
11
+ * How it works:
12
+ * - Keeps track of the latest incoming value
13
+ * - Ensures there is **at most one** pending RAF at a time
14
+ * - When the RAF fires, commits the **latest** value to state (`emitted`)
15
+ * - If additional changes arrive before the RAF runs, they are merged (the last write
16
+ * operation wins) and no new RAF is scheduled
17
+ *
18
+ * With this hook you can:
19
+ * - Feed a `FlatList`/`SectionList` from fast changing sources without spamming re-renders
20
+ * - Align React updates to the paint cadence (one publish per frame)
21
+ * - Help preserve item anchoring logic (e.g., MVCP) by reducing in-frame updates
22
+ *
23
+ * **Caveats:**
24
+ * - This hook intentionally skips intermediate states that occur within the same
25
+ * frame. If you must observe every transition (e.g., for analytics/reducers), do that
26
+ * upstream; this hook is for visual coalescing
27
+ * - Equality checks are simple referential equalities. If your producer recreates arrays
28
+ * or objects each time, you’ll still publish once per frame. To avoid even those
29
+ * emissions, stabilize upstream
30
+ * - This is not a silver bullet for throttle/debounce; it uses the screen’s refresh cycle;
31
+ * If you need “no more than once per X ms”, layer that upstream
32
+ *
33
+ * Usage tips:
34
+ * - Prefer passing already-memoized values when possible (e.g., stable arrays by ID).
35
+ * - Pair with a stable `keyExtractor` in lists so coalesced updates map cleanly to rows.
36
+ * - Do not cancel/reschedule on prop changes; cancellation is handled on unmount only.
37
+ *
38
+ * @param value The upstream value that may change multiple times within a single frame.
39
+ * @param isEnabled Determines whether the hook should be run or not (useful for cases where
40
+ * we want to conditionally use RAF when certain feature feature flags are enabled). If `false`,
41
+ * it will simply pass the data through (maintaining the reference as well).
42
+ * @returns A value that updates **at most once per frame** with the latest input.
43
+ */
44
+ export const useRAFCoalescedValue = <S>(value: S, isEnabled: boolean | undefined): S => {
45
+ const [emitted, setEmitted] = useState<S>(value);
46
+ const pendingRef = useRef<S>(value);
47
+ const rafIdRef = useRef<number | null>(null);
48
+
49
+ // If `value` changes, schedule a single RAF to publish the latest one.
50
+ useEffect(() => {
51
+ if (value === pendingRef.current || !isEnabled) return;
52
+ pendingRef.current = value;
53
+
54
+ // already scheduled the next frame, skip
55
+ if (rafIdRef.current) return;
56
+
57
+ const run = () => {
58
+ rafIdRef.current = null;
59
+ setEmitted(pendingRef.current);
60
+ };
61
+
62
+ rafIdRef.current = requestAnimationFrame(run);
63
+ }, [value, isEnabled]);
64
+
65
+ useEffect(() => {
66
+ return () => {
67
+ // cancel the frame if it exists only on unmount
68
+ if (rafIdRef.current) {
69
+ cancelAnimationFrame(rafIdRef.current);
70
+ rafIdRef.current = null;
71
+ }
72
+ };
73
+ }, []);
74
+
75
+ return isEnabled ? emitted : value;
76
+ };
package/src/version.json CHANGED
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "8.5.0"
2
+ "version": "8.5.1-beta.2"
3
3
  }