stream-chat-react-native-core 9.4.0-beta.12 → 9.4.0-beta.13

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 (61) hide show
  1. package/lib/commonjs/components/ImageGallery/ImageGallery.js +21 -76
  2. package/lib/commonjs/components/ImageGallery/ImageGallery.js.map +1 -1
  3. package/lib/commonjs/components/ImageGallery/components/AnimatedGalleryImage.js +6 -10
  4. package/lib/commonjs/components/ImageGallery/components/AnimatedGalleryImage.js.map +1 -1
  5. package/lib/commonjs/components/ImageGallery/components/AnimatedGalleryVideo.js +16 -14
  6. package/lib/commonjs/components/ImageGallery/components/AnimatedGalleryVideo.js.map +1 -1
  7. package/lib/commonjs/components/ImageGallery/components/ImageGalleryA11yProbe.js +67 -0
  8. package/lib/commonjs/components/ImageGallery/components/ImageGalleryA11yProbe.js.map +1 -0
  9. package/lib/commonjs/components/ImageGallery/hooks/useAnimatedGalleryStyle.js +4 -3
  10. package/lib/commonjs/components/ImageGallery/hooks/useAnimatedGalleryStyle.js.map +1 -1
  11. package/lib/commonjs/components/ImageGallery/hooks/useCurrentImageHeight.js +53 -0
  12. package/lib/commonjs/components/ImageGallery/hooks/useCurrentImageHeight.js.map +1 -0
  13. package/lib/commonjs/components/ImageGallery/hooks/useImageGalleryGestures.js +25 -31
  14. package/lib/commonjs/components/ImageGallery/hooks/useImageGalleryGestures.js.map +1 -1
  15. package/lib/commonjs/state-store/image-gallery-state-store.js +12 -3
  16. package/lib/commonjs/state-store/image-gallery-state-store.js.map +1 -1
  17. package/lib/commonjs/version.json +1 -1
  18. package/lib/module/components/ImageGallery/ImageGallery.js +21 -76
  19. package/lib/module/components/ImageGallery/ImageGallery.js.map +1 -1
  20. package/lib/module/components/ImageGallery/components/AnimatedGalleryImage.js +6 -10
  21. package/lib/module/components/ImageGallery/components/AnimatedGalleryImage.js.map +1 -1
  22. package/lib/module/components/ImageGallery/components/AnimatedGalleryVideo.js +16 -14
  23. package/lib/module/components/ImageGallery/components/AnimatedGalleryVideo.js.map +1 -1
  24. package/lib/module/components/ImageGallery/components/ImageGalleryA11yProbe.js +67 -0
  25. package/lib/module/components/ImageGallery/components/ImageGalleryA11yProbe.js.map +1 -0
  26. package/lib/module/components/ImageGallery/hooks/useAnimatedGalleryStyle.js +4 -3
  27. package/lib/module/components/ImageGallery/hooks/useAnimatedGalleryStyle.js.map +1 -1
  28. package/lib/module/components/ImageGallery/hooks/useCurrentImageHeight.js +53 -0
  29. package/lib/module/components/ImageGallery/hooks/useCurrentImageHeight.js.map +1 -0
  30. package/lib/module/components/ImageGallery/hooks/useImageGalleryGestures.js +25 -31
  31. package/lib/module/components/ImageGallery/hooks/useImageGalleryGestures.js.map +1 -1
  32. package/lib/module/state-store/image-gallery-state-store.js +12 -3
  33. package/lib/module/state-store/image-gallery-state-store.js.map +1 -1
  34. package/lib/module/version.json +1 -1
  35. package/lib/typescript/components/ImageGallery/ImageGallery.d.ts.map +1 -1
  36. package/lib/typescript/components/ImageGallery/components/AnimatedGalleryImage.d.ts.map +1 -1
  37. package/lib/typescript/components/ImageGallery/components/AnimatedGalleryVideo.d.ts +1 -4
  38. package/lib/typescript/components/ImageGallery/components/AnimatedGalleryVideo.d.ts.map +1 -1
  39. package/lib/typescript/components/ImageGallery/components/ImageGalleryA11yProbe.d.ts +20 -0
  40. package/lib/typescript/components/ImageGallery/components/ImageGalleryA11yProbe.d.ts.map +1 -0
  41. package/lib/typescript/components/ImageGallery/hooks/useAnimatedGalleryStyle.d.ts +2 -3
  42. package/lib/typescript/components/ImageGallery/hooks/useAnimatedGalleryStyle.d.ts.map +1 -1
  43. package/lib/typescript/components/ImageGallery/hooks/useCurrentImageHeight.d.ts +24 -0
  44. package/lib/typescript/components/ImageGallery/hooks/useCurrentImageHeight.d.ts.map +1 -0
  45. package/lib/typescript/components/ImageGallery/hooks/useImageGalleryGestures.d.ts +1 -1
  46. package/lib/typescript/components/ImageGallery/hooks/useImageGalleryGestures.d.ts.map +1 -1
  47. package/lib/typescript/state-store/image-gallery-state-store.d.ts +8 -0
  48. package/lib/typescript/state-store/image-gallery-state-store.d.ts.map +1 -1
  49. package/package.json +1 -1
  50. package/src/components/ImageGallery/ImageGallery.tsx +41 -120
  51. package/src/components/ImageGallery/__tests__/ImageGallery.test.tsx +4 -1
  52. package/src/components/ImageGallery/__tests__/ImageGalleryAdjustable.test.tsx +5 -4
  53. package/src/components/ImageGallery/components/AnimatedGalleryImage.tsx +9 -11
  54. package/src/components/ImageGallery/components/AnimatedGalleryVideo.tsx +28 -15
  55. package/src/components/ImageGallery/components/ImageGalleryA11yProbe.tsx +82 -0
  56. package/src/components/ImageGallery/hooks/useAnimatedGalleryStyle.tsx +5 -5
  57. package/src/components/ImageGallery/hooks/useCurrentImageHeight.ts +86 -0
  58. package/src/components/ImageGallery/hooks/useImageGalleryGestures.tsx +42 -51
  59. package/src/state-store/__tests__/image-gallery-state-store.test.ts +81 -0
  60. package/src/state-store/image-gallery-state-store.ts +27 -1
  61. package/src/version.json +1 -1
@@ -89,10 +89,13 @@ describe('ImageGallery', () => {
89
89
  render(
90
90
  <ImageGalleryComponent
91
91
  message={generateMessage({
92
+ // Video is placed at index 1 so it sits within the video-slide
93
+ // window (`shouldRender < 2`) — at index 2 it would render as a
94
+ // spacer instead of <AnimatedGalleryVideo>.
92
95
  attachments: [
93
96
  generateImageAttachment(),
94
- generateGiphyAttachment(),
95
97
  generateVideoAttachment({ type: 'video' }),
98
+ generateGiphyAttachment(),
96
99
  ],
97
100
  })}
98
101
  />,
@@ -95,13 +95,14 @@ describe('ImageGallery adjustable cycling', () => {
95
95
  });
96
96
  });
97
97
 
98
- it('does not apply the adjustable role when accessibility is disabled', async () => {
98
+ it('does not mount the a11y probe when accessibility is disabled', async () => {
99
99
  renderWithAssets(3, false);
100
100
 
101
+ // The probe is the sole element with this label; the parent ImageGallery
102
+ // renders only the visual background View when a11y is off, with no
103
+ // accessibility attributes to expose.
101
104
  await waitFor(() => {
102
- const root = findGalleryRoot();
103
- expect(root.props.accessibilityRole).toBeUndefined();
104
- expect(root.props.accessibilityActions).toBeUndefined();
105
+ expect(screen.queryByLabelText('Image Gallery', { includeHiddenElements: true })).toBeNull();
105
106
  });
106
107
  });
107
108
 
@@ -1,4 +1,4 @@
1
- import React, { useMemo } from 'react';
1
+ import React, { useCallback, useMemo } from 'react';
2
2
  import { StyleSheet, View } from 'react-native';
3
3
  import type { ImageStyle, StyleProp } from 'react-native';
4
4
  import Animated, { SharedValue } from 'react-native-reanimated';
@@ -30,10 +30,6 @@ type Props = {
30
30
  style?: StyleProp<ImageStyle>;
31
31
  };
32
32
 
33
- const imageGallerySelector = (state: ImageGalleryState) => ({
34
- currentIndex: state.currentIndex,
35
- });
36
-
37
33
  export const AnimatedGalleryImage = React.memo(
38
34
  (props: Props) => {
39
35
  const {
@@ -50,7 +46,13 @@ export const AnimatedGalleryImage = React.memo(
50
46
  } = props;
51
47
  const { imageGalleryStateStore } = useImageGalleryContext();
52
48
  const { resizableCDNHosts } = useChatConfigContext();
53
- const { currentIndex } = useStateStore(imageGalleryStateStore.state, imageGallerySelector);
49
+ const shouldRenderSelector = useCallback(
50
+ (state: ImageGalleryState) => ({
51
+ shouldRender: Math.abs(state.currentIndex - index) < 4,
52
+ }),
53
+ [index],
54
+ );
55
+ const { shouldRender } = useStateStore(imageGalleryStateStore.state, shouldRenderSelector);
54
56
 
55
57
  const uri = useMemo(() => {
56
58
  return getResizedImageUrl({
@@ -62,17 +64,13 @@ export const AnimatedGalleryImage = React.memo(
62
64
  }, [photo.uri, resizableCDNHosts, screenHeight, screenWidth]);
63
65
 
64
66
  const isSvg = useIsSvg(uri);
65
- const selected = currentIndex === index;
66
- const previous = currentIndex > index;
67
- const shouldRender = Math.abs(currentIndex - index) < 4;
68
67
 
69
68
  const animatedStyles = useAnimatedGalleryStyle({
69
+ currentIndexShared: imageGalleryStateStore.currentIndexShared,
70
70
  index,
71
71
  offsetScale,
72
- previous,
73
72
  scale,
74
73
  screenHeight,
75
- selected,
76
74
  translateX,
77
75
  translateY,
78
76
  });
@@ -1,4 +1,4 @@
1
- import React, { RefObject, useEffect, useRef, useState } from 'react';
1
+ import React, { RefObject, useCallback, useEffect, useRef, useState } from 'react';
2
2
  import { StyleSheet, View, ViewStyle } from 'react-native';
3
3
  import type { StyleProp } from 'react-native';
4
4
  import Animated, { SharedValue } from 'react-native-reanimated';
@@ -49,19 +49,14 @@ const styles = StyleSheet.create({
49
49
  },
50
50
  });
51
51
 
52
- const imageGallerySelector = (state: ImageGalleryState) => ({
53
- currentIndex: state.currentIndex,
54
- });
55
-
56
52
  const videoPlayerSelector = (state: VideoPlayerState) => ({
57
53
  currentPlaybackRate: state.currentPlaybackRate,
58
54
  isPlaying: state.isPlaying,
59
55
  });
60
56
 
61
- export const AnimatedGalleryVideo = (props: AnimatedGalleryVideoType) => {
57
+ const AnimatedGalleryVideoComponent = (props: AnimatedGalleryVideoType) => {
62
58
  const [opacity, setOpacity] = useState<number>(1);
63
59
  const { imageGalleryStateStore } = useImageGalleryContext();
64
- const { currentIndex } = useStateStore(imageGalleryStateStore.state, imageGallerySelector);
65
60
 
66
61
  const {
67
62
  attachmentId,
@@ -75,6 +70,14 @@ export const AnimatedGalleryVideo = (props: AnimatedGalleryVideoType) => {
75
70
  translateY,
76
71
  } = props;
77
72
 
73
+ const shouldRenderSelector = useCallback(
74
+ (state: ImageGalleryState) => ({
75
+ shouldRender: Math.abs(state.currentIndex - index) < 2,
76
+ }),
77
+ [index],
78
+ );
79
+ const { shouldRender } = useStateStore(imageGalleryStateStore.state, shouldRenderSelector);
80
+
78
81
  const videoRef = useRef<VideoType>(null);
79
82
 
80
83
  const videoPlayer = useImageGalleryVideoPlayer({
@@ -82,14 +85,15 @@ export const AnimatedGalleryVideo = (props: AnimatedGalleryVideoType) => {
82
85
  });
83
86
 
84
87
  useEffect(() => {
85
- if (videoRef.current) {
88
+ if (videoRef.current && shouldRender) {
86
89
  videoPlayer.initPlayer({ playerRef: videoRef.current });
87
90
  }
88
91
 
89
92
  return () => {
90
93
  videoPlayer.playerRef = null;
94
+ videoPlayer.onRemove();
91
95
  };
92
- }, [videoPlayer]);
96
+ }, [videoPlayer, shouldRender]);
93
97
 
94
98
  const { isPlaying, currentPlaybackRate } = useStateStore(videoPlayer.state, videoPlayerSelector);
95
99
 
@@ -147,17 +151,12 @@ export const AnimatedGalleryVideo = (props: AnimatedGalleryVideoType) => {
147
151
  }
148
152
  };
149
153
 
150
- const selected = currentIndex === index;
151
- const previous = currentIndex > index;
152
- const shouldRender = Math.abs(currentIndex - index) < 4;
153
-
154
154
  const animatedStyles = useAnimatedGalleryStyle({
155
+ currentIndexShared: imageGalleryStateStore.currentIndexShared,
155
156
  index,
156
157
  offsetScale,
157
- previous,
158
158
  scale,
159
159
  screenHeight,
160
- selected,
161
160
  translateX,
162
161
  translateY,
163
162
  });
@@ -216,4 +215,18 @@ export const AnimatedGalleryVideo = (props: AnimatedGalleryVideoType) => {
216
215
  );
217
216
  };
218
217
 
218
+ export const AnimatedGalleryVideo = React.memo(
219
+ AnimatedGalleryVideoComponent,
220
+ (prevProps, nextProps) => {
221
+ if (
222
+ prevProps.photo.uri === nextProps.photo.uri &&
223
+ prevProps.index === nextProps.index &&
224
+ prevProps.screenHeight === nextProps.screenHeight
225
+ ) {
226
+ return true;
227
+ }
228
+ return false;
229
+ },
230
+ );
231
+
219
232
  AnimatedGalleryVideo.displayName = 'AnimatedGalleryVideo';
@@ -0,0 +1,82 @@
1
+ import React, { useCallback, useMemo } from 'react';
2
+ import { StyleSheet, ViewStyle } from 'react-native';
3
+ import Animated, { AnimatedStyle } from 'react-native-reanimated';
4
+
5
+ import { useA11yLabel } from '../../../a11y/hooks/useA11yLabel';
6
+ import { useImageGalleryContext } from '../../../contexts/imageGalleryContext/ImageGalleryContext';
7
+ import { useStateStore } from '../../../hooks';
8
+ import { ImageGalleryState } from '../../../state-store/image-gallery-state-store';
9
+
10
+ const a11ySelector = (state: ImageGalleryState) => ({
11
+ assets: state.assets,
12
+ currentIndex: state.currentIndex,
13
+ });
14
+
15
+ type Props = {
16
+ containerBackground: AnimatedStyle<ViewStyle>;
17
+ };
18
+
19
+ /**
20
+ * Accessibility-enabled variant of the gallery's background fade view. Wires
21
+ * the "X of N" announcement and the VoiceOver/TalkBack increment/decrement
22
+ * actions. Subscribes locally to currentIndex + assets so the parent
23
+ * ImageGallery doesn't need that subscription itself — only this small sibling
24
+ * re-renders to refresh the accessibilityValue text on each swipe.
25
+ *
26
+ * Caller is responsible for gating this on whether accessibility is enabled.
27
+ * When disabled, the parent renders a plain Animated.View with the same
28
+ * background style and no a11y attributes.
29
+ */
30
+ export const ImageGalleryA11yProbe = ({ containerBackground }: Props) => {
31
+ const { imageGalleryStateStore } = useImageGalleryContext();
32
+ const { assets, currentIndex } = useStateStore(imageGalleryStateStore.state, a11ySelector);
33
+ const assetsCount = assets.length;
34
+
35
+ const accessibilityValueParams = useMemo(
36
+ () => ({ count: assetsCount, position: currentIndex + 1 }),
37
+ [currentIndex, assetsCount],
38
+ );
39
+ const accessibilityValueText = useA11yLabel(
40
+ 'a11y/{{position}} of {{count}}',
41
+ accessibilityValueParams,
42
+ );
43
+ const accessibilityValue = useMemo(
44
+ () => (accessibilityValueText ? { text: accessibilityValueText } : undefined),
45
+ [accessibilityValueText],
46
+ );
47
+ const adjustableActions = useMemo(
48
+ () => [{ name: 'increment' as const }, { name: 'decrement' as const }],
49
+ [],
50
+ );
51
+
52
+ const onAccessibilityAction = useCallback(
53
+ (event: { nativeEvent: { actionName: string } }) => {
54
+ const latest = imageGalleryStateStore.state.getLatestValue();
55
+ const latestCount = latest.assets.length;
56
+ const latestIndex = latest.currentIndex;
57
+ if (latestCount <= 1) return;
58
+ if (event.nativeEvent.actionName === 'increment') {
59
+ if (latestIndex < latestCount - 1) {
60
+ imageGalleryStateStore.currentIndex = latestIndex + 1;
61
+ }
62
+ } else if (event.nativeEvent.actionName === 'decrement') {
63
+ if (latestIndex > 0) {
64
+ imageGalleryStateStore.currentIndex = latestIndex - 1;
65
+ }
66
+ }
67
+ },
68
+ [imageGalleryStateStore],
69
+ );
70
+
71
+ return (
72
+ <Animated.View
73
+ accessible
74
+ accessibilityActions={adjustableActions}
75
+ accessibilityLabel='Image Gallery'
76
+ accessibilityRole='adjustable'
77
+ accessibilityValue={accessibilityValue}
78
+ onAccessibilityAction={onAccessibilityAction}
79
+ style={[StyleSheet.absoluteFill, containerBackground]}
80
+ />
81
+ );
82
+ };
@@ -4,12 +4,11 @@ import { SharedValue, useAnimatedStyle } from 'react-native-reanimated';
4
4
  import { useViewport } from '../../../hooks/useViewport';
5
5
 
6
6
  type Props = {
7
+ currentIndexShared: SharedValue<number>;
7
8
  index: number;
8
9
  offsetScale: SharedValue<number>;
9
- previous: boolean;
10
10
  scale: SharedValue<number>;
11
11
  screenHeight: number;
12
- selected: boolean;
13
12
  translateX: SharedValue<number>;
14
13
  translateY: SharedValue<number>;
15
14
  };
@@ -17,12 +16,11 @@ type Props = {
17
16
  const oneEighth = 1 / 8;
18
17
 
19
18
  export const useAnimatedGalleryStyle = ({
19
+ currentIndexShared,
20
20
  index,
21
21
  offsetScale,
22
- previous,
23
22
  scale,
24
23
  screenHeight,
25
- selected,
26
24
  translateX,
27
25
  translateY,
28
26
  }: Props) => {
@@ -40,6 +38,8 @@ export const useAnimatedGalleryStyle = ({
40
38
  * place as to not come into the screen when the image shrinks.
41
39
  */
42
40
  const animatedGalleryStyle = useAnimatedStyle<ImageStyle>(() => {
41
+ const selected = currentIndexShared.value === index;
42
+ const previous = currentIndexShared.value > index;
43
43
  const xScaleOffset = -7 * screenWidth * (0.5 + index);
44
44
  const yScaleOffset = -screenHeight * 3.5;
45
45
  return {
@@ -62,7 +62,7 @@ export const useAnimatedGalleryStyle = ({
62
62
  { scaleX: 1 },
63
63
  ],
64
64
  };
65
- }, [previous, selected]);
65
+ }, []);
66
66
 
67
67
  const animatedStyles = useAnimatedStyle(() => {
68
68
  const xScaleOffset = 7 * screenWidth * (0.5 + index);
@@ -0,0 +1,86 @@
1
+ import { useEffect } from 'react';
2
+ import { Image } from 'react-native';
3
+ import { SharedValue, useSharedValue } from 'react-native-reanimated';
4
+
5
+ import type {
6
+ ImageGalleryAsset,
7
+ ImageGalleryStateStore,
8
+ } from '../../../state-store/image-gallery-state-store';
9
+ import { FileTypes } from '../../../types/types';
10
+
11
+ /**
12
+ * Owns a SharedValue tracking the visible height of the currently selected
13
+ * gallery asset and keeps it in lockstep with the gallery store's
14
+ * currentIndex. Worklet consumers read `.value` on the UI thread.
15
+ *
16
+ * The store is subscribed via `subscribeWithSelector` (not `useStateStore`),
17
+ * so currentIndex changes never go through React state so no rerender is
18
+ * triggered in the calling component. The callback writes directly into the
19
+ * SharedValue.
20
+ *
21
+ * Sync path uses the attachment's intrinsic `original_height`/`original_width`
22
+ * when present. Async fallback via `Image.getSize` handles assets that don't
23
+ * carry dimensions; a token guards against the closure-race where the user
24
+ * swipes past a slide before its dimensions resolve.
25
+ */
26
+ export const useCurrentImageHeight = ({
27
+ assets,
28
+ fullWindowHeight,
29
+ fullWindowWidth,
30
+ imageGalleryStateStore,
31
+ }: {
32
+ assets: ImageGalleryAsset[];
33
+ fullWindowHeight: number;
34
+ fullWindowWidth: number;
35
+ imageGalleryStateStore: ImageGalleryStateStore;
36
+ }): SharedValue<number> => {
37
+ const currentImageHeight = useSharedValue(fullWindowHeight);
38
+
39
+ useEffect(() => {
40
+ let latestToken = 0;
41
+
42
+ const compute = (index: number) => {
43
+ const currentToken = ++latestToken;
44
+ const photo = assets[index];
45
+
46
+ if (photo?.original_height && photo?.original_width) {
47
+ const h = Math.floor(photo.original_height * (fullWindowWidth / photo.original_width));
48
+ currentImageHeight.value = h > fullWindowHeight ? fullWindowHeight : h;
49
+ return;
50
+ }
51
+
52
+ if (photo?.uri && photo.type === FileTypes.Image) {
53
+ Image.getSize(
54
+ photo.uri,
55
+ (width, height) => {
56
+ // Stale result, currentIndex moved on before getSize resolved.
57
+ if (currentToken !== latestToken) return;
58
+ if (!width || !height) {
59
+ currentImageHeight.value = fullWindowHeight;
60
+ return;
61
+ }
62
+ const imageHeight = Math.floor(height * (fullWindowWidth / width));
63
+ currentImageHeight.value =
64
+ imageHeight > fullWindowHeight ? fullWindowHeight : imageHeight;
65
+ },
66
+ () => {
67
+ if (currentToken !== latestToken) return;
68
+ currentImageHeight.value = fullWindowHeight;
69
+ },
70
+ );
71
+ return;
72
+ }
73
+
74
+ currentImageHeight.value = fullWindowHeight;
75
+ };
76
+
77
+ compute(imageGalleryStateStore.state.getLatestValue().currentIndex);
78
+
79
+ return imageGalleryStateStore.state.subscribeWithSelector(
80
+ (state) => ({ currentIndex: state.currentIndex }),
81
+ ({ currentIndex }) => compute(currentIndex),
82
+ );
83
+ }, [assets, fullWindowHeight, fullWindowWidth, currentImageHeight, imageGalleryStateStore]);
84
+
85
+ return currentImageHeight;
86
+ };
@@ -1,4 +1,4 @@
1
- import { useRef, useState } from 'react';
1
+ import { useRef } from 'react';
2
2
  import { Platform } from 'react-native';
3
3
  import { Gesture, GestureType } from 'react-native-gesture-handler';
4
4
  import {
@@ -14,9 +14,7 @@ import {
14
14
 
15
15
  import { useImageGalleryContext } from '../../../contexts/imageGalleryContext/ImageGalleryContext';
16
16
  import { useOverlayContext } from '../../../contexts/overlayContext/OverlayContext';
17
- import { useStateStore } from '../../../hooks';
18
17
  import { NativeHandlers } from '../../../native';
19
- import { ImageGalleryState } from '../../../state-store/image-gallery-state-store';
20
18
 
21
19
  export enum HasPinched {
22
20
  FALSE = 0,
@@ -31,10 +29,6 @@ export enum IsSwiping {
31
29
 
32
30
  const MARGIN = 32;
33
31
 
34
- const imageGallerySelector = (state: ImageGalleryState) => ({
35
- currentIndex: state.currentIndex,
36
- });
37
-
38
32
  export const useImageGalleryGestures = ({
39
33
  currentImageHeight,
40
34
  halfScreenHeight,
@@ -49,7 +43,7 @@ export const useImageGalleryGestures = ({
49
43
  translateY,
50
44
  translationX,
51
45
  }: {
52
- currentImageHeight: number;
46
+ currentImageHeight: SharedValue<number>;
53
47
  halfScreenHeight: number;
54
48
  halfScreenWidth: number;
55
49
  headerFooterVisible: SharedValue<number>;
@@ -62,20 +56,8 @@ export const useImageGalleryGestures = ({
62
56
  translateY: SharedValue<number>;
63
57
  translationX: SharedValue<number>;
64
58
  }) => {
65
- /**
66
- * if a specific image index > 0 has been passed in
67
- * while creating the hook, set the value of the index
68
- * reference to its value.
69
- *
70
- * This makes it possible to seelct an image in the list,
71
- * and scroll/pan as normal. Prior to this,
72
- * it was always assumed that one started at index 0 in the
73
- * gallery.
74
- * */
75
59
  const { imageGalleryStateStore } = useImageGalleryContext();
76
- const { currentIndex } = useStateStore(imageGalleryStateStore.state, imageGallerySelector);
77
-
78
- const [index, setIndex] = useState(currentIndex);
60
+ const currentIndexShared = imageGalleryStateStore.currentIndexShared;
79
61
 
80
62
  /**
81
63
  * Gesture handler refs
@@ -163,13 +145,16 @@ export const useImageGalleryGestures = ({
163
145
  };
164
146
 
165
147
  const moveToNextImage = () => {
166
- runOnJS(setIndex)(index + 1);
167
- imageGalleryStateStore.currentIndex = index + 1;
148
+ // Read fresh — both moveToNext/Previous are invoked via runOnJS from a
149
+ // worklet, so we'd otherwise be using a value captured at hook-creation
150
+ // time. The setter mirrors into currentIndexShared.
151
+ const currentIndex = imageGalleryStateStore.state.getLatestValue().currentIndex;
152
+ imageGalleryStateStore.currentIndex = currentIndex + 1;
168
153
  };
169
154
 
170
155
  const moveToPreviousImage = () => {
171
- runOnJS(setIndex)(index - 1);
172
- imageGalleryStateStore.currentIndex = index - 1;
156
+ const currentIndex = imageGalleryStateStore.state.getLatestValue().currentIndex;
157
+ imageGalleryStateStore.currentIndex = currentIndex - 1;
173
158
  };
174
159
 
175
160
  /**
@@ -221,7 +206,7 @@ export const useImageGalleryGestures = ({
221
206
  * true, or false and is reset on releasing the touch
222
207
  */
223
208
  if (isSwiping.value === IsSwiping.UNDETERMINED) {
224
- const maxXYRatio = isAndroid ? 1 : 0.25;
209
+ const maxXYRatio = 0.25;
225
210
  if (
226
211
  Math.abs(event.translationX / event.translationY) > maxXYRatio &&
227
212
  (Math.abs(-halfScreenWidth * (scale.value - 1) - offsetX.value) < 3 ||
@@ -261,16 +246,17 @@ export const useImageGalleryGestures = ({
261
246
  * If swiping down start scaling down the image for swipe
262
247
  * away effect
263
248
  */
249
+ const imageHeight = currentImageHeight.value;
264
250
  scale.value =
265
- currentImageHeight * offsetScale.value < screenHeight && translateY.value > 0
251
+ imageHeight * offsetScale.value < screenHeight && translateY.value > 0
266
252
  ? offsetScale.value * (1 - (1 / 3) * (translateY.value / screenHeight))
267
- : currentImageHeight * offsetScale.value > screenHeight &&
268
- translateY.value > (currentImageHeight / 2) * offsetScale.value - halfScreenHeight
253
+ : imageHeight * offsetScale.value > screenHeight &&
254
+ translateY.value > (imageHeight / 2) * offsetScale.value - halfScreenHeight
269
255
  ? offsetScale.value *
270
256
  (1 -
271
257
  (1 / 3) *
272
258
  ((translateY.value -
273
- ((currentImageHeight / 2) * offsetScale.value - halfScreenHeight)) /
259
+ ((imageHeight / 2) * offsetScale.value - halfScreenHeight)) /
274
260
  screenHeight))
275
261
  : scale.value;
276
262
 
@@ -287,6 +273,8 @@ export const useImageGalleryGestures = ({
287
273
  const finalXPosition = event.translationX - event.velocityX * 0.3;
288
274
  const finalYPosition = event.translationY + event.velocityY * 0.1;
289
275
 
276
+ const currentIndex = currentIndexShared.value;
277
+
290
278
  /**
291
279
  * If there is a next photo, the image is lined up to the right
292
280
  * edge, the swipe is to the left, and the final position is more
@@ -295,7 +283,7 @@ export const useImageGalleryGestures = ({
295
283
  * As we move towards the left to move to next image, the translationX value will be negative on X axis.
296
284
  */
297
285
  if (
298
- index < assetsLength - 1 &&
286
+ currentIndex < assetsLength - 1 &&
299
287
  Math.abs(halfScreenWidth * (scale.value - 1) + offsetX.value) < 3 &&
300
288
  translateX.value < 0 &&
301
289
  finalXPosition > halfScreenWidth &&
@@ -303,7 +291,7 @@ export const useImageGalleryGestures = ({
303
291
  ) {
304
292
  cancelAnimation(translationX);
305
293
  translationX.value = withTiming(
306
- -(screenWidth + MARGIN) * (index + 1),
294
+ -(screenWidth + MARGIN) * (currentIndex + 1),
307
295
  {
308
296
  duration: 200,
309
297
  easing: Easing.out(Easing.ease),
@@ -322,7 +310,7 @@ export const useImageGalleryGestures = ({
322
310
  * As we move towards the right to move to previous image, the translationX value will be positive on X axis.
323
311
  */
324
312
  } else if (
325
- index > 0 &&
313
+ currentIndex > 0 &&
326
314
  Math.abs(-halfScreenWidth * (scale.value - 1) + offsetX.value) < 3 &&
327
315
  translateX.value > 0 &&
328
316
  finalXPosition < -halfScreenWidth &&
@@ -330,7 +318,7 @@ export const useImageGalleryGestures = ({
330
318
  ) {
331
319
  cancelAnimation(translationX);
332
320
  translationX.value = withTiming(
333
- -(screenWidth + MARGIN) * (index - 1),
321
+ -(screenWidth + MARGIN) * (currentIndex - 1),
334
322
  {
335
323
  duration: 200,
336
324
  easing: Easing.out(Easing.ease),
@@ -376,19 +364,20 @@ export const useImageGalleryGestures = ({
376
364
  * otherwise use decay with a clamping at the edges to give the effect
377
365
  * the image is sliding along using velocity and friction
378
366
  */
367
+ const imageHeight = currentImageHeight.value;
379
368
  translateY.value =
380
- currentImageHeight * scale.value < screenHeight
369
+ imageHeight * scale.value < screenHeight
381
370
  ? withTiming(0, { reduceMotion: ReduceMotion.Never })
382
- : translateY.value > (currentImageHeight / 2) * scale.value - halfScreenHeight
383
- ? withTiming((currentImageHeight / 2) * scale.value - halfScreenHeight, {
371
+ : translateY.value > (imageHeight / 2) * scale.value - halfScreenHeight
372
+ ? withTiming((imageHeight / 2) * scale.value - halfScreenHeight, {
384
373
  reduceMotion: ReduceMotion.Never,
385
374
  })
386
- : translateY.value < (-currentImageHeight / 2) * scale.value + halfScreenHeight
387
- ? withTiming((-currentImageHeight / 2) * scale.value + halfScreenHeight)
375
+ : translateY.value < (-imageHeight / 2) * scale.value + halfScreenHeight
376
+ ? withTiming((-imageHeight / 2) * scale.value + halfScreenHeight)
388
377
  : withDecay({
389
378
  clamp: [
390
- (-currentImageHeight / 2) * scale.value + halfScreenHeight,
391
- (currentImageHeight / 2) * scale.value - halfScreenHeight,
379
+ (-imageHeight / 2) * scale.value + halfScreenHeight,
380
+ (imageHeight / 2) * scale.value - halfScreenHeight,
392
381
  ],
393
382
  deceleration: 0.99,
394
383
  velocity: event.velocityY,
@@ -411,7 +400,7 @@ export const useImageGalleryGestures = ({
411
400
  */
412
401
  if (
413
402
  finalYPosition > halfScreenHeight &&
414
- offsetY.value + 8 >= (currentImageHeight / 2) * scale.value - halfScreenHeight &&
403
+ offsetY.value + 8 >= (imageHeight / 2) * scale.value - halfScreenHeight &&
415
404
  isSwiping.value !== IsSwiping.TRUE &&
416
405
  translateY.value !== 0 &&
417
406
  !(
@@ -448,7 +437,7 @@ export const useImageGalleryGestures = ({
448
437
  ? withDecay({
449
438
  velocity: event.velocityY,
450
439
  })
451
- : withTiming(halfScreenHeight + (currentImageHeight / 2) * scale.value, {
440
+ : withTiming(halfScreenHeight + (imageHeight / 2) * scale.value, {
452
441
  duration: 200,
453
442
  easing: Easing.out(Easing.ease),
454
443
  });
@@ -662,13 +651,14 @@ export const useImageGalleryGestures = ({
662
651
  * edges of the screen return the photo to line up with the edges,
663
652
  * otherwise leave the photo in its current position
664
653
  */
654
+ const imageHeight = currentImageHeight.value;
665
655
  translateY.value =
666
- currentImageHeight * scale.value < screenHeight
656
+ imageHeight * scale.value < screenHeight
667
657
  ? withTiming(0)
668
- : translateY.value > (currentImageHeight / 2) * scale.value - screenHeight / 2
669
- ? withTiming((currentImageHeight / 2) * scale.value - screenHeight / 2)
670
- : translateY.value < (-currentImageHeight / 2) * scale.value + screenHeight / 2
671
- ? withTiming((-currentImageHeight / 2) * scale.value + screenHeight / 2)
658
+ : translateY.value > (imageHeight / 2) * scale.value - screenHeight / 2
659
+ ? withTiming((imageHeight / 2) * scale.value - screenHeight / 2)
660
+ : translateY.value < (-imageHeight / 2) * scale.value + screenHeight / 2
661
+ ? withTiming((-imageHeight / 2) * scale.value + screenHeight / 2)
672
662
  : translateY.value;
673
663
 
674
664
  /**
@@ -724,11 +714,12 @@ export const useImageGalleryGestures = ({
724
714
  duration: 200,
725
715
  easing: Easing.out(Easing.ease),
726
716
  });
727
- if (currentImageHeight * 2 > screenHeight) {
717
+ const imageHeight = currentImageHeight.value;
718
+ if (imageHeight * 2 > screenHeight) {
728
719
  const translateYTopBottom =
729
720
  event.absoluteY > halfScreenHeight
730
- ? -(currentImageHeight * 2 - screenHeight) / 2
731
- : (currentImageHeight * 2 - screenHeight) / 2;
721
+ ? -(imageHeight * 2 - screenHeight) / 2
722
+ : (imageHeight * 2 - screenHeight) / 2;
732
723
  translateY.value = withTiming(translateYTopBottom, {
733
724
  duration: 200,
734
725
  easing: Easing.out(Easing.ease),