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.
- package/lib/commonjs/components/ImageGallery/ImageGallery.js +21 -76
- package/lib/commonjs/components/ImageGallery/ImageGallery.js.map +1 -1
- package/lib/commonjs/components/ImageGallery/components/AnimatedGalleryImage.js +6 -10
- package/lib/commonjs/components/ImageGallery/components/AnimatedGalleryImage.js.map +1 -1
- package/lib/commonjs/components/ImageGallery/components/AnimatedGalleryVideo.js +16 -14
- package/lib/commonjs/components/ImageGallery/components/AnimatedGalleryVideo.js.map +1 -1
- package/lib/commonjs/components/ImageGallery/components/ImageGalleryA11yProbe.js +67 -0
- package/lib/commonjs/components/ImageGallery/components/ImageGalleryA11yProbe.js.map +1 -0
- package/lib/commonjs/components/ImageGallery/hooks/useAnimatedGalleryStyle.js +4 -3
- package/lib/commonjs/components/ImageGallery/hooks/useAnimatedGalleryStyle.js.map +1 -1
- package/lib/commonjs/components/ImageGallery/hooks/useCurrentImageHeight.js +53 -0
- package/lib/commonjs/components/ImageGallery/hooks/useCurrentImageHeight.js.map +1 -0
- package/lib/commonjs/components/ImageGallery/hooks/useImageGalleryGestures.js +25 -31
- package/lib/commonjs/components/ImageGallery/hooks/useImageGalleryGestures.js.map +1 -1
- package/lib/commonjs/state-store/image-gallery-state-store.js +12 -3
- package/lib/commonjs/state-store/image-gallery-state-store.js.map +1 -1
- package/lib/commonjs/version.json +1 -1
- package/lib/module/components/ImageGallery/ImageGallery.js +21 -76
- package/lib/module/components/ImageGallery/ImageGallery.js.map +1 -1
- package/lib/module/components/ImageGallery/components/AnimatedGalleryImage.js +6 -10
- package/lib/module/components/ImageGallery/components/AnimatedGalleryImage.js.map +1 -1
- package/lib/module/components/ImageGallery/components/AnimatedGalleryVideo.js +16 -14
- package/lib/module/components/ImageGallery/components/AnimatedGalleryVideo.js.map +1 -1
- package/lib/module/components/ImageGallery/components/ImageGalleryA11yProbe.js +67 -0
- package/lib/module/components/ImageGallery/components/ImageGalleryA11yProbe.js.map +1 -0
- package/lib/module/components/ImageGallery/hooks/useAnimatedGalleryStyle.js +4 -3
- package/lib/module/components/ImageGallery/hooks/useAnimatedGalleryStyle.js.map +1 -1
- package/lib/module/components/ImageGallery/hooks/useCurrentImageHeight.js +53 -0
- package/lib/module/components/ImageGallery/hooks/useCurrentImageHeight.js.map +1 -0
- package/lib/module/components/ImageGallery/hooks/useImageGalleryGestures.js +25 -31
- package/lib/module/components/ImageGallery/hooks/useImageGalleryGestures.js.map +1 -1
- package/lib/module/state-store/image-gallery-state-store.js +12 -3
- package/lib/module/state-store/image-gallery-state-store.js.map +1 -1
- package/lib/module/version.json +1 -1
- package/lib/typescript/components/ImageGallery/ImageGallery.d.ts.map +1 -1
- package/lib/typescript/components/ImageGallery/components/AnimatedGalleryImage.d.ts.map +1 -1
- package/lib/typescript/components/ImageGallery/components/AnimatedGalleryVideo.d.ts +1 -4
- package/lib/typescript/components/ImageGallery/components/AnimatedGalleryVideo.d.ts.map +1 -1
- package/lib/typescript/components/ImageGallery/components/ImageGalleryA11yProbe.d.ts +20 -0
- package/lib/typescript/components/ImageGallery/components/ImageGalleryA11yProbe.d.ts.map +1 -0
- package/lib/typescript/components/ImageGallery/hooks/useAnimatedGalleryStyle.d.ts +2 -3
- package/lib/typescript/components/ImageGallery/hooks/useAnimatedGalleryStyle.d.ts.map +1 -1
- package/lib/typescript/components/ImageGallery/hooks/useCurrentImageHeight.d.ts +24 -0
- package/lib/typescript/components/ImageGallery/hooks/useCurrentImageHeight.d.ts.map +1 -0
- package/lib/typescript/components/ImageGallery/hooks/useImageGalleryGestures.d.ts +1 -1
- package/lib/typescript/components/ImageGallery/hooks/useImageGalleryGestures.d.ts.map +1 -1
- package/lib/typescript/state-store/image-gallery-state-store.d.ts +8 -0
- package/lib/typescript/state-store/image-gallery-state-store.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/ImageGallery/ImageGallery.tsx +41 -120
- package/src/components/ImageGallery/__tests__/ImageGallery.test.tsx +4 -1
- package/src/components/ImageGallery/__tests__/ImageGalleryAdjustable.test.tsx +5 -4
- package/src/components/ImageGallery/components/AnimatedGalleryImage.tsx +9 -11
- package/src/components/ImageGallery/components/AnimatedGalleryVideo.tsx +28 -15
- package/src/components/ImageGallery/components/ImageGalleryA11yProbe.tsx +82 -0
- package/src/components/ImageGallery/hooks/useAnimatedGalleryStyle.tsx +5 -5
- package/src/components/ImageGallery/hooks/useCurrentImageHeight.ts +86 -0
- package/src/components/ImageGallery/hooks/useImageGalleryGestures.tsx +42 -51
- package/src/state-store/__tests__/image-gallery-state-store.test.ts +81 -0
- package/src/state-store/image-gallery-state-store.ts +27 -1
- 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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
}, [
|
|
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
|
|
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
|
|
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
|
|
167
|
-
|
|
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
|
-
|
|
172
|
-
imageGalleryStateStore.currentIndex =
|
|
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 =
|
|
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
|
-
|
|
251
|
+
imageHeight * offsetScale.value < screenHeight && translateY.value > 0
|
|
266
252
|
? offsetScale.value * (1 - (1 / 3) * (translateY.value / screenHeight))
|
|
267
|
-
:
|
|
268
|
-
translateY.value > (
|
|
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
|
-
((
|
|
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
|
-
|
|
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) * (
|
|
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
|
-
|
|
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) * (
|
|
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
|
-
|
|
369
|
+
imageHeight * scale.value < screenHeight
|
|
381
370
|
? withTiming(0, { reduceMotion: ReduceMotion.Never })
|
|
382
|
-
: translateY.value > (
|
|
383
|
-
? withTiming((
|
|
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 < (-
|
|
387
|
-
? withTiming((-
|
|
375
|
+
: translateY.value < (-imageHeight / 2) * scale.value + halfScreenHeight
|
|
376
|
+
? withTiming((-imageHeight / 2) * scale.value + halfScreenHeight)
|
|
388
377
|
: withDecay({
|
|
389
378
|
clamp: [
|
|
390
|
-
(-
|
|
391
|
-
(
|
|
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 >= (
|
|
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 + (
|
|
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
|
-
|
|
656
|
+
imageHeight * scale.value < screenHeight
|
|
667
657
|
? withTiming(0)
|
|
668
|
-
: translateY.value > (
|
|
669
|
-
? withTiming((
|
|
670
|
-
: translateY.value < (-
|
|
671
|
-
? withTiming((-
|
|
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
|
-
|
|
717
|
+
const imageHeight = currentImageHeight.value;
|
|
718
|
+
if (imageHeight * 2 > screenHeight) {
|
|
728
719
|
const translateYTopBottom =
|
|
729
720
|
event.absoluteY > halfScreenHeight
|
|
730
|
-
? -(
|
|
731
|
-
: (
|
|
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),
|