stream-chat-react-native-core 9.6.1-beta.5 → 9.6.1-beta.8

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 (26) hide show
  1. package/lib/commonjs/components/KeyboardCompatibleView/KeyboardCompatibleView.js +22 -2
  2. package/lib/commonjs/components/KeyboardCompatibleView/KeyboardCompatibleView.js.map +1 -1
  3. package/lib/commonjs/components/MessageInput/MessageComposer.js +2 -2
  4. package/lib/commonjs/components/MessageInput/MessageComposer.js.map +1 -1
  5. package/lib/commonjs/contexts/overlayContext/MessageOverlayHostLayer.js +61 -11
  6. package/lib/commonjs/contexts/overlayContext/MessageOverlayHostLayer.js.map +1 -1
  7. package/lib/commonjs/version.json +1 -1
  8. package/lib/module/components/KeyboardCompatibleView/KeyboardCompatibleView.js +22 -2
  9. package/lib/module/components/KeyboardCompatibleView/KeyboardCompatibleView.js.map +1 -1
  10. package/lib/module/components/MessageInput/MessageComposer.js +2 -2
  11. package/lib/module/components/MessageInput/MessageComposer.js.map +1 -1
  12. package/lib/module/contexts/overlayContext/MessageOverlayHostLayer.js +61 -11
  13. package/lib/module/contexts/overlayContext/MessageOverlayHostLayer.js.map +1 -1
  14. package/lib/module/version.json +1 -1
  15. package/lib/typescript/components/KeyboardCompatibleView/KeyboardCompatibleView.d.ts.map +1 -1
  16. package/lib/typescript/components/MessageInput/MessageComposer.d.ts.map +1 -1
  17. package/lib/typescript/contexts/overlayContext/MessageOverlayHostLayer.d.ts.map +1 -1
  18. package/package.json +2 -2
  19. package/src/components/KeyboardCompatibleView/KeyboardCompatibleView.tsx +58 -3
  20. package/src/components/MessageInput/MessageComposer.tsx +2 -3
  21. package/src/components/MessageInput/__tests__/__snapshots__/AttachButton.test.tsx.snap +36 -36
  22. package/src/components/MessageInput/__tests__/__snapshots__/SendButton.test.tsx.snap +19 -19
  23. package/src/components/Thread/__tests__/__snapshots__/Thread.test.tsx.snap +6 -6
  24. package/src/contexts/overlayContext/MessageOverlayHostLayer.tsx +92 -12
  25. package/src/contexts/overlayContext/__tests__/MessageOverlayHostLayer.test.tsx +62 -6
  26. package/src/version.json +1 -1
@@ -38,9 +38,9 @@ exports[`AttachButton should call handleAttachButtonPress when the button is cli
38
38
  <View
39
39
  handlerTags={
40
40
  [
41
+ 37,
42
+ 36,
41
43
  35,
42
- 34,
43
- 33,
44
44
  ]
45
45
  }
46
46
  onGestureHandlerEvent={[Function]}
@@ -212,9 +212,9 @@ exports[`AttachButton should call handleAttachButtonPress when the button is cli
212
212
  <View
213
213
  handlerTags={
214
214
  [
215
+ 40,
216
+ 39,
215
217
  38,
216
- 37,
217
- 36,
218
218
  ]
219
219
  }
220
220
  onGestureHandlerEvent={[Function]}
@@ -359,9 +359,9 @@ exports[`AttachButton should call handleAttachButtonPress when the button is cli
359
359
  <View
360
360
  handlerTags={
361
361
  [
362
+ 43,
363
+ 42,
362
364
  41,
363
- 40,
364
- 39,
365
365
  ]
366
366
  }
367
367
  onGestureHandlerEvent={[Function]}
@@ -535,9 +535,9 @@ exports[`AttachButton should call handleAttachButtonPress when the button is cli
535
535
  <View
536
536
  handlerTags={
537
537
  [
538
+ 46,
539
+ 45,
538
540
  44,
539
- 43,
540
- 42,
541
541
  ]
542
542
  }
543
543
  onGestureHandlerEvent={[Function]}
@@ -678,9 +678,9 @@ exports[`AttachButton should call handleAttachButtonPress when the button is cli
678
678
  <View
679
679
  handlerTags={
680
680
  [
681
+ 49,
682
+ 48,
681
683
  47,
682
- 46,
683
- 45,
684
684
  ]
685
685
  }
686
686
  onGestureHandlerEvent={[Function]}
@@ -844,17 +844,16 @@ exports[`AttachButton should call handleAttachButtonPress when the button is cli
844
844
  }
845
845
  >
846
846
  <View
847
+ collapsable={false}
847
848
  style={
848
849
  {
849
850
  "height": 0,
850
- "opacity": 0,
851
- "width": 0,
852
851
  }
853
852
  }
854
- testID="message-overlay-top"
853
+ testID="message-overlay-message"
855
854
  >
856
855
  <View
857
- name="top-item"
856
+ name="message-overlay"
858
857
  style={
859
858
  {
860
859
  "bottom": 0,
@@ -867,16 +866,17 @@ exports[`AttachButton should call handleAttachButtonPress when the button is cli
867
866
  />
868
867
  </View>
869
868
  <View
870
- pointerEvents="box-none"
871
869
  style={
872
870
  {
873
871
  "height": 0,
872
+ "opacity": 0,
873
+ "width": 0,
874
874
  }
875
875
  }
876
- testID="message-overlay-message"
876
+ testID="message-overlay-top"
877
877
  >
878
878
  <View
879
- name="message-overlay"
879
+ name="top-item"
880
880
  style={
881
881
  {
882
882
  "bottom": 0,
@@ -954,9 +954,9 @@ exports[`AttachButton should render a enabled AttachButton 1`] = `
954
954
  <View
955
955
  handlerTags={
956
956
  [
957
+ 20,
957
958
  19,
958
959
  18,
959
- 17,
960
960
  ]
961
961
  }
962
962
  onGestureHandlerEvent={[Function]}
@@ -1128,9 +1128,9 @@ exports[`AttachButton should render a enabled AttachButton 1`] = `
1128
1128
  <View
1129
1129
  handlerTags={
1130
1130
  [
1131
+ 23,
1131
1132
  22,
1132
1133
  21,
1133
- 20,
1134
1134
  ]
1135
1135
  }
1136
1136
  onGestureHandlerEvent={[Function]}
@@ -1275,9 +1275,9 @@ exports[`AttachButton should render a enabled AttachButton 1`] = `
1275
1275
  <View
1276
1276
  handlerTags={
1277
1277
  [
1278
+ 26,
1278
1279
  25,
1279
1280
  24,
1280
- 23,
1281
1281
  ]
1282
1282
  }
1283
1283
  onGestureHandlerEvent={[Function]}
@@ -1451,9 +1451,9 @@ exports[`AttachButton should render a enabled AttachButton 1`] = `
1451
1451
  <View
1452
1452
  handlerTags={
1453
1453
  [
1454
+ 29,
1454
1455
  28,
1455
1456
  27,
1456
- 26,
1457
1457
  ]
1458
1458
  }
1459
1459
  onGestureHandlerEvent={[Function]}
@@ -1594,9 +1594,9 @@ exports[`AttachButton should render a enabled AttachButton 1`] = `
1594
1594
  <View
1595
1595
  handlerTags={
1596
1596
  [
1597
+ 32,
1597
1598
  31,
1598
1599
  30,
1599
- 29,
1600
1600
  ]
1601
1601
  }
1602
1602
  onGestureHandlerEvent={[Function]}
@@ -1760,17 +1760,16 @@ exports[`AttachButton should render a enabled AttachButton 1`] = `
1760
1760
  }
1761
1761
  >
1762
1762
  <View
1763
+ collapsable={false}
1763
1764
  style={
1764
1765
  {
1765
1766
  "height": 0,
1766
- "opacity": 0,
1767
- "width": 0,
1768
1767
  }
1769
1768
  }
1770
- testID="message-overlay-top"
1769
+ testID="message-overlay-message"
1771
1770
  >
1772
1771
  <View
1773
- name="top-item"
1772
+ name="message-overlay"
1774
1773
  style={
1775
1774
  {
1776
1775
  "bottom": 0,
@@ -1783,16 +1782,17 @@ exports[`AttachButton should render a enabled AttachButton 1`] = `
1783
1782
  />
1784
1783
  </View>
1785
1784
  <View
1786
- pointerEvents="box-none"
1787
1785
  style={
1788
1786
  {
1789
1787
  "height": 0,
1788
+ "opacity": 0,
1789
+ "width": 0,
1790
1790
  }
1791
1791
  }
1792
- testID="message-overlay-message"
1792
+ testID="message-overlay-top"
1793
1793
  >
1794
1794
  <View
1795
- name="message-overlay"
1795
+ name="top-item"
1796
1796
  style={
1797
1797
  {
1798
1798
  "bottom": 0,
@@ -2676,17 +2676,16 @@ exports[`AttachButton should render an disabled AttachButton 1`] = `
2676
2676
  }
2677
2677
  >
2678
2678
  <View
2679
+ collapsable={false}
2679
2680
  style={
2680
2681
  {
2681
2682
  "height": 0,
2682
- "opacity": 0,
2683
- "width": 0,
2684
2683
  }
2685
2684
  }
2686
- testID="message-overlay-top"
2685
+ testID="message-overlay-message"
2687
2686
  >
2688
2687
  <View
2689
- name="top-item"
2688
+ name="message-overlay"
2690
2689
  style={
2691
2690
  {
2692
2691
  "bottom": 0,
@@ -2699,16 +2698,17 @@ exports[`AttachButton should render an disabled AttachButton 1`] = `
2699
2698
  />
2700
2699
  </View>
2701
2700
  <View
2702
- pointerEvents="box-none"
2703
2701
  style={
2704
2702
  {
2705
2703
  "height": 0,
2704
+ "opacity": 0,
2705
+ "width": 0,
2706
2706
  }
2707
2707
  }
2708
- testID="message-overlay-message"
2708
+ testID="message-overlay-top"
2709
2709
  >
2710
2710
  <View
2711
- name="message-overlay"
2711
+ name="top-item"
2712
2712
  style={
2713
2713
  {
2714
2714
  "bottom": 0,
@@ -842,17 +842,16 @@ exports[`SendButton should render a SendButton 1`] = `
842
842
  }
843
843
  >
844
844
  <View
845
+ collapsable={false}
845
846
  style={
846
847
  {
847
848
  "height": 0,
848
- "opacity": 0,
849
- "width": 0,
850
849
  }
851
850
  }
852
- testID="message-overlay-top"
851
+ testID="message-overlay-message"
853
852
  >
854
853
  <View
855
- name="top-item"
854
+ name="message-overlay"
856
855
  style={
857
856
  {
858
857
  "bottom": 0,
@@ -865,16 +864,17 @@ exports[`SendButton should render a SendButton 1`] = `
865
864
  />
866
865
  </View>
867
866
  <View
868
- pointerEvents="box-none"
869
867
  style={
870
868
  {
871
869
  "height": 0,
870
+ "opacity": 0,
871
+ "width": 0,
872
872
  }
873
873
  }
874
- testID="message-overlay-message"
874
+ testID="message-overlay-top"
875
875
  >
876
876
  <View
877
- name="message-overlay"
877
+ name="top-item"
878
878
  style={
879
879
  {
880
880
  "bottom": 0,
@@ -952,9 +952,9 @@ exports[`SendButton should render a disabled SendButton 1`] = `
952
952
  <View
953
953
  handlerTags={
954
954
  [
955
+ 20,
955
956
  19,
956
957
  18,
957
- 17,
958
958
  ]
959
959
  }
960
960
  onGestureHandlerEvent={[Function]}
@@ -1124,9 +1124,9 @@ exports[`SendButton should render a disabled SendButton 1`] = `
1124
1124
  <View
1125
1125
  handlerTags={
1126
1126
  [
1127
+ 23,
1127
1128
  22,
1128
1129
  21,
1129
- 20,
1130
1130
  ]
1131
1131
  }
1132
1132
  onGestureHandlerEvent={[Function]}
@@ -1271,9 +1271,9 @@ exports[`SendButton should render a disabled SendButton 1`] = `
1271
1271
  <View
1272
1272
  handlerTags={
1273
1273
  [
1274
+ 26,
1274
1275
  25,
1275
1276
  24,
1276
- 23,
1277
1277
  ]
1278
1278
  }
1279
1279
  onGestureHandlerEvent={[Function]}
@@ -1447,9 +1447,9 @@ exports[`SendButton should render a disabled SendButton 1`] = `
1447
1447
  <View
1448
1448
  handlerTags={
1449
1449
  [
1450
+ 29,
1450
1451
  28,
1451
1452
  27,
1452
- 26,
1453
1453
  ]
1454
1454
  }
1455
1455
  onGestureHandlerEvent={[Function]}
@@ -1590,9 +1590,9 @@ exports[`SendButton should render a disabled SendButton 1`] = `
1590
1590
  <View
1591
1591
  handlerTags={
1592
1592
  [
1593
+ 32,
1593
1594
  31,
1594
1595
  30,
1595
- 29,
1596
1596
  ]
1597
1597
  }
1598
1598
  onGestureHandlerEvent={[Function]}
@@ -1756,17 +1756,16 @@ exports[`SendButton should render a disabled SendButton 1`] = `
1756
1756
  }
1757
1757
  >
1758
1758
  <View
1759
+ collapsable={false}
1759
1760
  style={
1760
1761
  {
1761
1762
  "height": 0,
1762
- "opacity": 0,
1763
- "width": 0,
1764
1763
  }
1765
1764
  }
1766
- testID="message-overlay-top"
1765
+ testID="message-overlay-message"
1767
1766
  >
1768
1767
  <View
1769
- name="top-item"
1768
+ name="message-overlay"
1770
1769
  style={
1771
1770
  {
1772
1771
  "bottom": 0,
@@ -1779,16 +1778,17 @@ exports[`SendButton should render a disabled SendButton 1`] = `
1779
1778
  />
1780
1779
  </View>
1781
1780
  <View
1782
- pointerEvents="box-none"
1783
1781
  style={
1784
1782
  {
1785
1783
  "height": 0,
1784
+ "opacity": 0,
1785
+ "width": 0,
1786
1786
  }
1787
1787
  }
1788
- testID="message-overlay-message"
1788
+ testID="message-overlay-top"
1789
1789
  >
1790
1790
  <View
1791
- name="message-overlay"
1791
+ name="top-item"
1792
1792
  style={
1793
1793
  {
1794
1794
  "bottom": 0,
@@ -1833,9 +1833,9 @@ exports[`Thread should match thread snapshot 1`] = `
1833
1833
  <View
1834
1834
  handlerTags={
1835
1835
  [
1836
+ 27,
1836
1837
  26,
1837
1838
  25,
1838
- 24,
1839
1839
  ]
1840
1840
  }
1841
1841
  onGestureHandlerEvent={[Function]}
@@ -2211,9 +2211,9 @@ exports[`Thread should match thread snapshot 1`] = `
2211
2211
  <View
2212
2212
  handlerTags={
2213
2213
  [
2214
+ 30,
2214
2215
  29,
2215
2216
  28,
2216
- 27,
2217
2217
  ]
2218
2218
  }
2219
2219
  onGestureHandlerEvent={[Function]}
@@ -2393,9 +2393,9 @@ exports[`Thread should match thread snapshot 1`] = `
2393
2393
  <View
2394
2394
  handlerTags={
2395
2395
  [
2396
+ 33,
2396
2397
  32,
2397
2398
  31,
2398
- 30,
2399
2399
  ]
2400
2400
  }
2401
2401
  onGestureHandlerEvent={[Function]}
@@ -2540,9 +2540,9 @@ exports[`Thread should match thread snapshot 1`] = `
2540
2540
  <View
2541
2541
  handlerTags={
2542
2542
  [
2543
+ 36,
2543
2544
  35,
2544
2545
  34,
2545
- 33,
2546
2546
  ]
2547
2547
  }
2548
2548
  onGestureHandlerEvent={[Function]}
@@ -2716,9 +2716,9 @@ exports[`Thread should match thread snapshot 1`] = `
2716
2716
  <View
2717
2717
  handlerTags={
2718
2718
  [
2719
+ 39,
2719
2720
  38,
2720
2721
  37,
2721
- 36,
2722
2722
  ]
2723
2723
  }
2724
2724
  onGestureHandlerEvent={[Function]}
@@ -2859,9 +2859,9 @@ exports[`Thread should match thread snapshot 1`] = `
2859
2859
  <View
2860
2860
  handlerTags={
2861
2861
  [
2862
+ 42,
2862
2863
  41,
2863
2864
  40,
2864
- 39,
2865
2865
  ]
2866
2866
  }
2867
2867
  onGestureHandlerEvent={[Function]}
@@ -12,11 +12,13 @@ import {
12
12
  import { Gesture, GestureDetector } from 'react-native-gesture-handler';
13
13
  import Animated, {
14
14
  type AnimatedStyle,
15
+ cancelAnimation,
15
16
  clamp,
16
17
  runOnJS,
17
18
  useAnimatedStyle,
18
19
  useDerivedValue,
19
20
  useSharedValue,
21
+ withDecay,
20
22
  withSpring,
21
23
  } from 'react-native-reanimated';
22
24
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
@@ -118,6 +120,41 @@ export const MessageOverlayHostLayer = () => {
118
120
  const minY = topInset + padding;
119
121
  const maxY = screenH - bottomInset - padding;
120
122
 
123
+ // When top + message + bottom don't fit the usable area, a Pan gesture drives `scrollY` and
124
+ // the message is translated by it (see hostStyle). No ScrollView, all on the UI thread.
125
+ const scrollY = useSharedValue(0);
126
+ const scrollStartY = useSharedValue(0);
127
+
128
+ const scrollGeom = useDerivedValue(() => {
129
+ if (!messageH.value || !topH.value || !bottomH.value) {
130
+ return { needsScroll: false };
131
+ }
132
+ const available = maxY - minY;
133
+ const contentH = topH.value.h + messageH.value.h + bottomH.value.h;
134
+ return { needsScroll: contentH > available };
135
+ });
136
+
137
+ // How far the message can scroll: the amount its content overflows the usable area. At max
138
+ // scroll the message tail lands just above the sticky footer.
139
+ const maxScroll = useDerivedValue(() => {
140
+ if (!scrollGeom.value.needsScroll || !messageH.value || !topH.value || !bottomH.value) {
141
+ return 0;
142
+ }
143
+ const available = maxY - minY;
144
+ const contentH = topH.value.h + messageH.value.h + bottomH.value.h;
145
+ return Math.max(0, contentH - available);
146
+ });
147
+
148
+ // On close, freeze any inflight fling (the message closes from where it was scrolled - see
149
+ // `hostStyle`) and only reset the offset once fully closed, ready for the next open.
150
+ useEffect(() => {
151
+ if (closing) {
152
+ cancelAnimation(scrollY);
153
+ } else if (!isActive) {
154
+ scrollY.value = 0;
155
+ }
156
+ }, [closing, isActive, scrollY]);
157
+
121
158
  const backdrop = useSharedValue(0);
122
159
  const closeCoverOpacity = useSharedValue(0);
123
160
 
@@ -171,6 +208,12 @@ export const MessageOverlayHostLayer = () => {
171
208
  const anchorY = messageH.value.y;
172
209
  const msgH = messageH.value.h;
173
210
  const minTop = minY + topH.value.h;
211
+
212
+ // When scrolling, pin the message directly under the (pinned) top item; scrollY does the rest.
213
+ if (scrollGeom.value.needsScroll) {
214
+ return minTop - anchorY;
215
+ }
216
+
174
217
  const maxTopWithBottom = maxY - (msgH + bottomH.value.h);
175
218
  const canFitBottomWithoutOverlap = minTop <= maxTopWithBottom;
176
219
  const solvedTop = canFitBottomWithoutOverlap
@@ -186,6 +229,12 @@ export const MessageOverlayHostLayer = () => {
186
229
  const anchorMessageTop = messageH.value.y;
187
230
  const msgH = messageH.value.h;
188
231
  const minMessageTop = minY + topH.value.h;
232
+
233
+ // When scrolling, pin the bottom item as a sticky footer at the bottom of the usable area.
234
+ if (scrollGeom.value.needsScroll) {
235
+ return maxY - bottomH.value.h - bottomH.value.y;
236
+ }
237
+
189
238
  const maxMessageTopWithBottom = maxY - (msgH + bottomH.value.h);
190
239
  const canFitBottomWithoutOverlap = minMessageTop <= maxMessageTopWithBottom;
191
240
  const solvedMessageTop = canFitBottomWithoutOverlap
@@ -234,19 +283,47 @@ export const MessageOverlayHostLayer = () => {
234
283
  };
235
284
  });
236
285
 
286
+ // Two composed `translateY`: first is the springed open/close position, second is the raw
287
+ // scroll offset. On close we fold the scroll into the first (springed) target so the message
288
+ // animates back from where it was scrolled, while the latter stays put.
237
289
  const hostStyle = useAnimatedStyle(() => {
238
290
  if (!messageH.value) return { height: 0 };
239
- const translateY = isActive ? (closing ? closeCorrectionY.value : messageShiftY.value) : 0;
291
+ const scroll = scrollGeom.value.needsScroll ? scrollY.value : 0;
292
+ const translateY = isActive
293
+ ? closing
294
+ ? closeCorrectionY.value + scroll
295
+ : messageShiftY.value
296
+ : 0;
240
297
  return {
241
298
  height: messageH.value.h,
242
299
  left: messageH.value.x,
243
300
  position: 'absolute',
244
301
  top: messageH.value.y,
245
- transform: [{ translateY: withSpring(translateY, { duration: DURATION }) }],
302
+ transform: [
303
+ { translateY: withSpring(translateY, { duration: DURATION }) },
304
+ { translateY: scroll === 0 ? 0 : -scroll },
305
+ ],
246
306
  width: messageH.value.w,
247
307
  };
248
308
  });
249
309
 
310
+ // Drag scrolls the message (clamped to `[0, maxScroll]`); release flings with decay. When the
311
+ // content fits, `maxScroll` is 0 so this is a noop.
312
+ const pan = useMemo(
313
+ () =>
314
+ Gesture.Pan()
315
+ .onBegin(() => {
316
+ scrollStartY.value = scrollY.value;
317
+ })
318
+ .onUpdate((e) => {
319
+ scrollY.value = clamp(scrollStartY.value - e.translationY, 0, maxScroll.value);
320
+ })
321
+ .onEnd((e) => {
322
+ scrollY.value = withDecay({ clamp: [0, maxScroll.value], velocity: -e.velocityY });
323
+ }),
324
+ [maxScroll, scrollStartY, scrollY],
325
+ );
326
+
250
327
  const tap = useMemo(
251
328
  () =>
252
329
  Gesture.Tap()
@@ -279,8 +356,9 @@ export const MessageOverlayHostLayer = () => {
279
356
  })
280
357
  .onEnd(() => {
281
358
  runOnJS(closeOverlay)();
282
- }),
283
- [bottomH, bottomShiftY, messageShiftY, topH],
359
+ })
360
+ .requireExternalGestureToFail(pan),
361
+ [bottomH, bottomShiftY, messageShiftY, pan, topH],
284
362
  );
285
363
 
286
364
  return (
@@ -314,18 +392,20 @@ export const MessageOverlayHostLayer = () => {
314
392
  />
315
393
  ) : (
316
394
  <>
395
+ {/*
396
+ Message renders first (lowest z) so it sits behind the opaque top/bottom items;
397
+ a scrolled/overflowing message passes behind them instead of being clipped.
398
+ */}
399
+ <GestureDetector gesture={pan}>
400
+ <Animated.View style={hostStyle} testID='message-overlay-message'>
401
+ <PortalHost name='message-overlay' style={styles.absoluteFill} />
402
+ </Animated.View>
403
+ </GestureDetector>
404
+
317
405
  <Animated.View style={topItemStyle} testID='message-overlay-top'>
318
406
  <PortalHost name='top-item' style={styles.absoluteFill} />
319
407
  </Animated.View>
320
408
 
321
- <Animated.View
322
- pointerEvents='box-none'
323
- style={hostStyle}
324
- testID='message-overlay-message'
325
- >
326
- <PortalHost name='message-overlay' style={styles.absoluteFill} />
327
- </Animated.View>
328
-
329
409
  <Animated.View style={bottomItemStyle} testID='message-overlay-bottom'>
330
410
  <PortalHost name='bottom-item' style={styles.absoluteFill} />
331
411
  </Animated.View>