stream-chat-react-native-core 4.6.1 → 4.7.0-beta.1

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 (200) hide show
  1. package/lib/commonjs/components/Attachment/Attachment.js +22 -13
  2. package/lib/commonjs/components/Attachment/Attachment.js.map +1 -1
  3. package/lib/commonjs/components/Attachment/Gallery.js +72 -37
  4. package/lib/commonjs/components/Attachment/Gallery.js.map +1 -1
  5. package/lib/commonjs/components/Attachment/VideoThumbnail.js +83 -0
  6. package/lib/commonjs/components/Attachment/VideoThumbnail.js.map +1 -0
  7. package/lib/commonjs/components/Attachment/hooks/useGoToURL.js +1 -1
  8. package/lib/commonjs/components/Attachment/hooks/useGoToURL.js.map +1 -1
  9. package/lib/commonjs/components/Attachment/utils/buildGallery/buildThumbnail.js +1 -0
  10. package/lib/commonjs/components/Attachment/utils/buildGallery/buildThumbnail.js.map +1 -1
  11. package/lib/commonjs/components/Attachment/utils/getAspectRatio.js +5 -5
  12. package/lib/commonjs/components/Attachment/utils/getAspectRatio.js.map +1 -1
  13. package/lib/commonjs/components/Channel/Channel.js +19 -14
  14. package/lib/commonjs/components/Channel/Channel.js.map +1 -1
  15. package/lib/commonjs/components/Channel/hooks/useCreateMessagesContext.js +4 -2
  16. package/lib/commonjs/components/Channel/hooks/useCreateMessagesContext.js.map +1 -1
  17. package/lib/commonjs/components/ChannelPreview/hooks/useChannelPreviewDisplayName.js +43 -28
  18. package/lib/commonjs/components/ChannelPreview/hooks/useChannelPreviewDisplayName.js.map +1 -1
  19. package/lib/commonjs/components/ImageGallery/ImageGallery.js +167 -345
  20. package/lib/commonjs/components/ImageGallery/ImageGallery.js.map +1 -1
  21. package/lib/commonjs/components/ImageGallery/components/AnimatedGalleryImage.js +4 -4
  22. package/lib/commonjs/components/ImageGallery/components/AnimatedGalleryImage.js.map +1 -1
  23. package/lib/commonjs/components/ImageGallery/components/AnimatedGalleryVideo.js +225 -0
  24. package/lib/commonjs/components/ImageGallery/components/AnimatedGalleryVideo.js.map +1 -0
  25. package/lib/commonjs/components/ImageGallery/components/ImageGalleryFooter.js +102 -40
  26. package/lib/commonjs/components/ImageGallery/components/ImageGalleryFooter.js.map +1 -1
  27. package/lib/commonjs/components/ImageGallery/components/ImageGalleryVideoControl.js +145 -0
  28. package/lib/commonjs/components/ImageGallery/components/ImageGalleryVideoControl.js.map +1 -0
  29. package/lib/commonjs/components/ImageGallery/components/ImageGrid.js +31 -34
  30. package/lib/commonjs/components/ImageGallery/components/ImageGrid.js.map +1 -1
  31. package/lib/commonjs/components/ImageGallery/components/ProgressControl.js +153 -0
  32. package/lib/commonjs/components/ImageGallery/components/ProgressControl.js.map +1 -0
  33. package/lib/commonjs/components/ImageGallery/hooks/useImageGalleryGestures.js +373 -0
  34. package/lib/commonjs/components/ImageGallery/hooks/useImageGalleryGestures.js.map +1 -0
  35. package/lib/commonjs/components/Message/Message.js +23 -13
  36. package/lib/commonjs/components/Message/Message.js.map +1 -1
  37. package/lib/commonjs/components/Message/hooks/useCreateMessageContext.js +4 -2
  38. package/lib/commonjs/components/Message/hooks/useCreateMessageContext.js.map +1 -1
  39. package/lib/commonjs/components/MessageOverlay/MessageOverlay.js +25 -23
  40. package/lib/commonjs/components/MessageOverlay/MessageOverlay.js.map +1 -1
  41. package/lib/commonjs/components/Reply/Reply.js +32 -12
  42. package/lib/commonjs/components/Reply/Reply.js.map +1 -1
  43. package/lib/commonjs/components/Spinner/Spinner.js +12 -4
  44. package/lib/commonjs/components/Spinner/Spinner.js.map +1 -1
  45. package/lib/commonjs/components/index.js +26 -0
  46. package/lib/commonjs/components/index.js.map +1 -1
  47. package/lib/commonjs/contexts/messageContext/MessageContext.js +2 -2
  48. package/lib/commonjs/contexts/messageContext/MessageContext.js.map +1 -1
  49. package/lib/commonjs/contexts/messageOverlayContext/MessageOverlayContext.js +2 -2
  50. package/lib/commonjs/contexts/messageOverlayContext/MessageOverlayContext.js.map +1 -1
  51. package/lib/commonjs/contexts/messagesContext/MessagesContext.js +2 -2
  52. package/lib/commonjs/contexts/messagesContext/MessagesContext.js.map +1 -1
  53. package/lib/commonjs/contexts/themeContext/utils/theme.js +10 -1
  54. package/lib/commonjs/contexts/themeContext/utils/theme.js.map +1 -1
  55. package/lib/commonjs/icons/Pause.js +61 -0
  56. package/lib/commonjs/icons/Pause.js.map +1 -0
  57. package/lib/commonjs/icons/Play.js +42 -0
  58. package/lib/commonjs/icons/Play.js.map +1 -0
  59. package/lib/commonjs/icons/index.js +26 -0
  60. package/lib/commonjs/icons/index.js.map +1 -1
  61. package/lib/commonjs/mock-builders/api/queryMembers.js +98 -1
  62. package/lib/commonjs/mock-builders/api/queryMembers.js.map +1 -1
  63. package/lib/commonjs/native.js +7 -1
  64. package/lib/commonjs/native.js.map +1 -1
  65. package/lib/commonjs/utils/getUrlOfImageAttachment.js +1 -1
  66. package/lib/commonjs/utils/getUrlOfImageAttachment.js.map +1 -1
  67. package/lib/commonjs/version.json +1 -1
  68. package/lib/module/components/Attachment/Attachment.js +22 -13
  69. package/lib/module/components/Attachment/Attachment.js.map +1 -1
  70. package/lib/module/components/Attachment/Gallery.js +72 -37
  71. package/lib/module/components/Attachment/Gallery.js.map +1 -1
  72. package/lib/module/components/Attachment/VideoThumbnail.js +83 -0
  73. package/lib/module/components/Attachment/VideoThumbnail.js.map +1 -0
  74. package/lib/module/components/Attachment/hooks/useGoToURL.js +1 -1
  75. package/lib/module/components/Attachment/hooks/useGoToURL.js.map +1 -1
  76. package/lib/module/components/Attachment/utils/buildGallery/buildThumbnail.js +1 -0
  77. package/lib/module/components/Attachment/utils/buildGallery/buildThumbnail.js.map +1 -1
  78. package/lib/module/components/Attachment/utils/getAspectRatio.js +5 -5
  79. package/lib/module/components/Attachment/utils/getAspectRatio.js.map +1 -1
  80. package/lib/module/components/Channel/Channel.js +19 -14
  81. package/lib/module/components/Channel/Channel.js.map +1 -1
  82. package/lib/module/components/Channel/hooks/useCreateMessagesContext.js +4 -2
  83. package/lib/module/components/Channel/hooks/useCreateMessagesContext.js.map +1 -1
  84. package/lib/module/components/ChannelPreview/hooks/useChannelPreviewDisplayName.js +43 -28
  85. package/lib/module/components/ChannelPreview/hooks/useChannelPreviewDisplayName.js.map +1 -1
  86. package/lib/module/components/ImageGallery/ImageGallery.js +167 -345
  87. package/lib/module/components/ImageGallery/ImageGallery.js.map +1 -1
  88. package/lib/module/components/ImageGallery/components/AnimatedGalleryImage.js +4 -4
  89. package/lib/module/components/ImageGallery/components/AnimatedGalleryImage.js.map +1 -1
  90. package/lib/module/components/ImageGallery/components/AnimatedGalleryVideo.js +225 -0
  91. package/lib/module/components/ImageGallery/components/AnimatedGalleryVideo.js.map +1 -0
  92. package/lib/module/components/ImageGallery/components/ImageGalleryFooter.js +102 -40
  93. package/lib/module/components/ImageGallery/components/ImageGalleryFooter.js.map +1 -1
  94. package/lib/module/components/ImageGallery/components/ImageGalleryVideoControl.js +145 -0
  95. package/lib/module/components/ImageGallery/components/ImageGalleryVideoControl.js.map +1 -0
  96. package/lib/module/components/ImageGallery/components/ImageGrid.js +31 -34
  97. package/lib/module/components/ImageGallery/components/ImageGrid.js.map +1 -1
  98. package/lib/module/components/ImageGallery/components/ProgressControl.js +153 -0
  99. package/lib/module/components/ImageGallery/components/ProgressControl.js.map +1 -0
  100. package/lib/module/components/ImageGallery/hooks/useImageGalleryGestures.js +373 -0
  101. package/lib/module/components/ImageGallery/hooks/useImageGalleryGestures.js.map +1 -0
  102. package/lib/module/components/Message/Message.js +23 -13
  103. package/lib/module/components/Message/Message.js.map +1 -1
  104. package/lib/module/components/Message/hooks/useCreateMessageContext.js +4 -2
  105. package/lib/module/components/Message/hooks/useCreateMessageContext.js.map +1 -1
  106. package/lib/module/components/MessageOverlay/MessageOverlay.js +25 -23
  107. package/lib/module/components/MessageOverlay/MessageOverlay.js.map +1 -1
  108. package/lib/module/components/Reply/Reply.js +32 -12
  109. package/lib/module/components/Reply/Reply.js.map +1 -1
  110. package/lib/module/components/Spinner/Spinner.js +12 -4
  111. package/lib/module/components/Spinner/Spinner.js.map +1 -1
  112. package/lib/module/components/index.js +26 -0
  113. package/lib/module/components/index.js.map +1 -1
  114. package/lib/module/contexts/messageContext/MessageContext.js +2 -2
  115. package/lib/module/contexts/messageContext/MessageContext.js.map +1 -1
  116. package/lib/module/contexts/messageOverlayContext/MessageOverlayContext.js +2 -2
  117. package/lib/module/contexts/messageOverlayContext/MessageOverlayContext.js.map +1 -1
  118. package/lib/module/contexts/messagesContext/MessagesContext.js +2 -2
  119. package/lib/module/contexts/messagesContext/MessagesContext.js.map +1 -1
  120. package/lib/module/contexts/themeContext/utils/theme.js +10 -1
  121. package/lib/module/contexts/themeContext/utils/theme.js.map +1 -1
  122. package/lib/module/icons/Pause.js +61 -0
  123. package/lib/module/icons/Pause.js.map +1 -0
  124. package/lib/module/icons/Play.js +42 -0
  125. package/lib/module/icons/Play.js.map +1 -0
  126. package/lib/module/icons/index.js +26 -0
  127. package/lib/module/icons/index.js.map +1 -1
  128. package/lib/module/mock-builders/api/queryMembers.js +98 -1
  129. package/lib/module/mock-builders/api/queryMembers.js.map +1 -1
  130. package/lib/module/native.js +7 -1
  131. package/lib/module/native.js.map +1 -1
  132. package/lib/module/utils/getUrlOfImageAttachment.js +1 -1
  133. package/lib/module/utils/getUrlOfImageAttachment.js.map +1 -1
  134. package/lib/module/version.json +1 -1
  135. package/lib/typescript/components/Attachment/Gallery.d.ts +1 -1
  136. package/lib/typescript/components/Attachment/VideoThumbnail.d.ts +4 -0
  137. package/lib/typescript/components/Attachment/utils/buildGallery/types.d.ts +1 -0
  138. package/lib/typescript/components/Attachment/utils/getAspectRatio.d.ts +1 -1
  139. package/lib/typescript/components/Channel/Channel.d.ts +1 -1
  140. package/lib/typescript/components/Channel/hooks/useCreateMessagesContext.d.ts +1 -1
  141. package/lib/typescript/components/ChannelPreview/hooks/__tests__/useChannelPreviewDisplayName.test.d.ts +1 -0
  142. package/lib/typescript/components/ChannelPreview/hooks/useChannelPreviewDisplayName.d.ts +4 -4
  143. package/lib/typescript/components/ImageGallery/ImageGallery.d.ts +2 -0
  144. package/lib/typescript/components/ImageGallery/components/AnimatedGalleryVideo.d.ts +27 -0
  145. package/lib/typescript/components/ImageGallery/components/ImageGalleryFooter.d.ts +18 -2
  146. package/lib/typescript/components/ImageGallery/components/ImageGalleryVideoControl.d.ts +3 -0
  147. package/lib/typescript/components/ImageGallery/components/ProgressControl.d.ts +5 -0
  148. package/lib/typescript/components/ImageGallery/hooks/useImageGalleryGestures.d.ts +36 -0
  149. package/lib/typescript/components/Message/hooks/useCreateMessageContext.d.ts +1 -1
  150. package/lib/typescript/components/Spinner/Spinner.d.ts +7 -1
  151. package/lib/typescript/components/index.d.ts +2 -0
  152. package/lib/typescript/contexts/messageContext/MessageContext.d.ts +3 -1
  153. package/lib/typescript/contexts/messageOverlayContext/MessageOverlayContext.d.ts +1 -0
  154. package/lib/typescript/contexts/messagesContext/MessagesContext.d.ts +2 -0
  155. package/lib/typescript/contexts/themeContext/utils/theme.d.ts +9 -0
  156. package/lib/typescript/icons/Pause.d.ts +3 -0
  157. package/lib/typescript/icons/Play.d.ts +3 -0
  158. package/lib/typescript/icons/index.d.ts +2 -0
  159. package/lib/typescript/mock-builders/api/queryMembers.d.ts +161 -0
  160. package/lib/typescript/mock-builders/api/utils.d.ts +7 -0
  161. package/lib/typescript/mock-builders/generator/user.d.ts +30 -0
  162. package/lib/typescript/mock-builders/mock.d.ts +3 -0
  163. package/lib/typescript/native.d.ts +66 -1
  164. package/package.json +1 -1
  165. package/src/components/Attachment/Attachment.tsx +11 -7
  166. package/src/components/Attachment/Gallery.tsx +75 -36
  167. package/src/components/Attachment/VideoThumbnail.tsx +45 -0
  168. package/src/components/Attachment/hooks/useGoToURL.ts +1 -1
  169. package/src/components/Attachment/utils/buildGallery/buildThumbnail.ts +1 -0
  170. package/src/components/Attachment/utils/buildGallery/types.ts +1 -0
  171. package/src/components/Attachment/utils/getAspectRatio.ts +7 -5
  172. package/src/components/Channel/Channel.tsx +4 -0
  173. package/src/components/Channel/hooks/useCreateMessagesContext.ts +2 -0
  174. package/src/components/ChannelPreview/hooks/__tests__/useChannelPreviewDisplayName.test.tsx +83 -0
  175. package/src/components/ChannelPreview/hooks/useChannelPreviewDisplayName.ts +43 -26
  176. package/src/components/ImageGallery/ImageGallery.tsx +145 -667
  177. package/src/components/ImageGallery/components/AnimatedGalleryImage.tsx +4 -4
  178. package/src/components/ImageGallery/components/AnimatedGalleryVideo.tsx +224 -0
  179. package/src/components/ImageGallery/components/ImageGalleryFooter.tsx +114 -19
  180. package/src/components/ImageGallery/components/ImageGalleryVideoControl.tsx +103 -0
  181. package/src/components/ImageGallery/components/ImageGrid.tsx +12 -25
  182. package/src/components/ImageGallery/components/ProgressControl.tsx +107 -0
  183. package/src/components/ImageGallery/hooks/useImageGalleryGestures.tsx +722 -0
  184. package/src/components/Message/Message.tsx +10 -3
  185. package/src/components/Message/hooks/useCreateMessageContext.ts +2 -0
  186. package/src/components/MessageOverlay/MessageOverlay.tsx +2 -0
  187. package/src/components/Reply/Reply.tsx +36 -23
  188. package/src/components/Spinner/Spinner.tsx +11 -4
  189. package/src/components/index.ts +2 -0
  190. package/src/contexts/messageContext/MessageContext.tsx +2 -0
  191. package/src/contexts/messageOverlayContext/MessageOverlayContext.tsx +1 -0
  192. package/src/contexts/messagesContext/MessagesContext.tsx +2 -0
  193. package/src/contexts/themeContext/utils/theme.ts +18 -0
  194. package/src/icons/Pause.tsx +14 -0
  195. package/src/icons/Play.tsx +17 -0
  196. package/src/icons/index.ts +2 -0
  197. package/src/mock-builders/api/queryMembers.js +98 -0
  198. package/src/native.ts +59 -1
  199. package/src/utils/getUrlOfImageAttachment.ts +1 -1
  200. package/src/version.json +1 -1
@@ -0,0 +1,722 @@
1
+ import { Platform } from 'react-native';
2
+ import type {
3
+ PanGestureHandlerGestureEvent,
4
+ PinchGestureHandlerGestureEvent,
5
+ TapGestureHandlerGestureEvent,
6
+ } from 'react-native-gesture-handler';
7
+ import {
8
+ cancelAnimation,
9
+ Easing,
10
+ runOnJS,
11
+ SharedValue,
12
+ useAnimatedGestureHandler,
13
+ useSharedValue,
14
+ withDecay,
15
+ withTiming,
16
+ } from 'react-native-reanimated';
17
+
18
+ import { useOverlayContext } from '../../../contexts/overlayContext/OverlayContext';
19
+ import { triggerHaptic } from '../../../native';
20
+
21
+ export enum HasPinched {
22
+ FALSE = 0,
23
+ TRUE,
24
+ }
25
+
26
+ export enum IsSwiping {
27
+ UNDETERMINED = 0,
28
+ TRUE,
29
+ FALSE,
30
+ }
31
+
32
+ const MARGIN = 32;
33
+
34
+ export const useImageGalleryGestures = ({
35
+ currentImageHeight,
36
+ halfScreenHeight,
37
+ halfScreenWidth,
38
+ headerFooterVisible,
39
+ offsetScale,
40
+ overlayOpacity,
41
+ photoLength,
42
+ scale,
43
+ screenHeight,
44
+ screenWidth,
45
+ setSelectedIndex,
46
+ translateX,
47
+ translateY,
48
+ translationX,
49
+ }: {
50
+ currentImageHeight: number;
51
+ halfScreenHeight: number;
52
+ halfScreenWidth: number;
53
+ headerFooterVisible: SharedValue<number>;
54
+ offsetScale: SharedValue<number>;
55
+ overlayOpacity: SharedValue<number>;
56
+ photoLength: number;
57
+ scale: SharedValue<number>;
58
+ screenHeight: number;
59
+ screenWidth: number;
60
+ setSelectedIndex: React.Dispatch<React.SetStateAction<number>>;
61
+ translateX: SharedValue<number>;
62
+ translateY: SharedValue<number>;
63
+ translationX: SharedValue<number>;
64
+ }) => {
65
+ const { setOverlay } = useOverlayContext();
66
+ const isAndroid = Platform.OS === 'android';
67
+
68
+ /**
69
+ * Values to track scale for haptic feedback firing
70
+ */
71
+ const hasHitBottomScale = useSharedValue(1);
72
+ const hasHitTopScale = useSharedValue(0);
73
+
74
+ /**
75
+ * Shared values for touch tracking
76
+ */
77
+ const originX = useSharedValue(0);
78
+ const originY = useSharedValue(0);
79
+ const oldFocalX = useSharedValue(0);
80
+ const oldFocalY = useSharedValue(0);
81
+ const focalX = useSharedValue(0);
82
+ const focalY = useSharedValue(0);
83
+ const index = useSharedValue(0);
84
+
85
+ /**
86
+ * Shared values for movement
87
+ */
88
+ const offsetX = useSharedValue(0);
89
+ const offsetY = useSharedValue(0);
90
+
91
+ /**
92
+ * Shared values for touch tracking
93
+ */
94
+ const focalOffsetX = useSharedValue(0);
95
+ const focalOffsetY = useSharedValue(0);
96
+ const adjustedFocalX = useSharedValue(0);
97
+ const adjustedFocalY = useSharedValue(0);
98
+ const tapX = useSharedValue(0);
99
+ const tapY = useSharedValue(0);
100
+
101
+ /**
102
+ * Shared values for gesture tracking
103
+ */
104
+ const numberOfPinchFingers = useSharedValue(0);
105
+ const isSwiping = useSharedValue(0);
106
+ const isPinch = useSharedValue(false);
107
+ const hasPinched = useSharedValue(0);
108
+
109
+ /**
110
+ * Reset gesture values for use on touch release
111
+ */
112
+ const resetTouchValues = () => {
113
+ 'worklet';
114
+ focalX.value = 0;
115
+ focalY.value = 0;
116
+ oldFocalX.value = 0;
117
+ oldFocalY.value = 0;
118
+ originX.value = 0;
119
+ originY.value = 0;
120
+ focalOffsetX.value = 0;
121
+ focalOffsetY.value = 0;
122
+ numberOfPinchFingers.value = 0;
123
+ isPinch.value = false;
124
+ isSwiping.value = IsSwiping.UNDETERMINED;
125
+ };
126
+
127
+ /**
128
+ * Reset movement values for use on selected photo change
129
+ */
130
+ const resetMovementValues = () => {
131
+ 'worklet';
132
+ translateX.value = 0;
133
+ translateY.value = 0;
134
+ scale.value = 1;
135
+ offsetScale.value = 1;
136
+ };
137
+
138
+ /**
139
+ * We use simultaneousHandlers to allow pan and pinch gesture handlers
140
+ * depending on the gesture. The touch is fully handled by the pinch
141
+ * gesture handler once it has began so all interactions by the pan
142
+ * handler are blocked if isPinch.value is true
143
+ */
144
+ const onPan = useAnimatedGestureHandler<PanGestureHandlerGestureEvent>(
145
+ {
146
+ onActive: (evt) => {
147
+ if (evt.numberOfPointers === 1 && !isPinch.value) {
148
+ /**
149
+ * If Android where a second finger cannot be added back and
150
+ * removing one finger returns to Pan Handler then adjust origin
151
+ * on finger remove and set swiping false
152
+ */
153
+ if (isAndroid && hasPinched.value === HasPinched.TRUE) {
154
+ hasPinched.value = HasPinched.FALSE;
155
+ isSwiping.value = IsSwiping.FALSE;
156
+ offsetX.value = translateX.value + evt.translationX;
157
+ offsetY.value = translateY.value - evt.translationY;
158
+ }
159
+
160
+ /**
161
+ * isSwiping is used to prevent Y movement if a clear swipe to next
162
+ * or previous is begun when at the edge of a photo. The value is
163
+ * either 0, 1, or 2, via the IsSwiping enum designating undetermined,
164
+ * true, or false and is reset on releasing the touch
165
+ */
166
+ if (isSwiping.value === IsSwiping.UNDETERMINED) {
167
+ const maxXYRatio = isAndroid ? 1 : 0.25;
168
+ if (
169
+ Math.abs(evt.translationX / evt.translationY) > maxXYRatio &&
170
+ (Math.abs(-halfScreenWidth * (scale.value - 1) - offsetX.value) < 3 ||
171
+ Math.abs(halfScreenWidth * (scale.value - 1) - offsetX.value) < 3)
172
+ ) {
173
+ isSwiping.value = IsSwiping.TRUE;
174
+ }
175
+ if (Math.abs(evt.translationY) > 25) {
176
+ isSwiping.value = IsSwiping.FALSE;
177
+ }
178
+ }
179
+
180
+ /**
181
+ * localEvtScale is used for swipe away
182
+ */
183
+ const localEvtScale = scale.value / offsetScale.value;
184
+
185
+ /**
186
+ * If not swiping translate the image in X and Y to the event
187
+ * translation plus offset. If swiping only translate X, if
188
+ * swiping down when at top of screen or centered balance scale
189
+ * using offset to a degree (this needs improvement the calculation
190
+ * is incorrect but likely needs origin use to be 100%)
191
+ */
192
+ translateX.value =
193
+ scale.value !== offsetScale.value
194
+ ? offsetX.value * localEvtScale - evt.translationX
195
+ : offsetX.value - evt.translationX;
196
+ translateY.value =
197
+ isSwiping.value !== IsSwiping.TRUE
198
+ ? scale.value !== offsetScale.value
199
+ ? offsetY.value * localEvtScale + evt.translationY
200
+ : offsetY.value + evt.translationY
201
+ : translateY.value;
202
+
203
+ /**
204
+ * If swiping down start scaling down the image for swipe
205
+ * away effect
206
+ */
207
+ scale.value =
208
+ currentImageHeight * offsetScale.value < screenHeight && translateY.value > 0
209
+ ? offsetScale.value * (1 - (1 / 3) * (translateY.value / screenHeight))
210
+ : currentImageHeight * offsetScale.value > screenHeight &&
211
+ translateY.value > (currentImageHeight / 2) * offsetScale.value - halfScreenHeight
212
+ ? offsetScale.value *
213
+ (1 -
214
+ (1 / 3) *
215
+ ((translateY.value -
216
+ ((currentImageHeight / 2) * offsetScale.value - halfScreenHeight)) /
217
+ screenHeight))
218
+ : scale.value;
219
+
220
+ overlayOpacity.value = localEvtScale;
221
+ }
222
+ },
223
+ onFinish: (evt) => {
224
+ if (!isPinch.value && evt.numberOfPointers < 2) {
225
+ /**
226
+ * To determine if the fling should page to the next image we
227
+ * calculate a final position based on the current position and
228
+ * event velocity
229
+ */
230
+ const finalXPosition = evt.translationX - evt.velocityX * 0.3;
231
+ const finalYPosition = evt.translationY + evt.velocityY * 0.1;
232
+
233
+ /**
234
+ * If there is a next photo, the image is lined up to the right
235
+ * edge, the swipe is to the left, and the final position is more
236
+ * than half the screen width, move to the next image
237
+ */
238
+ if (
239
+ index.value < photoLength - 1 &&
240
+ Math.abs(halfScreenWidth * (scale.value - 1) + offsetX.value) < 3 &&
241
+ translateX.value < 0 &&
242
+ finalXPosition < -halfScreenWidth &&
243
+ isSwiping.value === IsSwiping.TRUE
244
+ ) {
245
+ cancelAnimation(translationX);
246
+ translationX.value = withTiming(
247
+ -(screenWidth + MARGIN) * (index.value + 1),
248
+ {
249
+ duration: 200,
250
+ easing: Easing.out(Easing.ease),
251
+ },
252
+ () => {
253
+ resetMovementValues();
254
+ index.value = index.value + 1;
255
+ runOnJS(setSelectedIndex)(index.value);
256
+ },
257
+ );
258
+
259
+ /**
260
+ * If there is a previous photo, the image is lined up to the left
261
+ * edge, the swipe is to the right, and the final position is more
262
+ * than half the screen width, move to the previous image
263
+ */
264
+ } else if (
265
+ index.value > 0 &&
266
+ Math.abs(-halfScreenWidth * (scale.value - 1) + offsetX.value) < 3 &&
267
+ translateX.value > 0 &&
268
+ finalXPosition > halfScreenWidth &&
269
+ isSwiping.value === IsSwiping.TRUE
270
+ ) {
271
+ cancelAnimation(translationX);
272
+ translationX.value = withTiming(
273
+ -(screenWidth + MARGIN) * (index.value - 1),
274
+ {
275
+ duration: 200,
276
+ easing: Easing.out(Easing.ease),
277
+ },
278
+ () => {
279
+ resetMovementValues();
280
+ index.value = index.value - 1;
281
+ runOnJS(setSelectedIndex)(index.value);
282
+ },
283
+ );
284
+ }
285
+
286
+ /**
287
+ * When the pan is finished if the scale is less than 1 return the
288
+ * photo to center, if the photo is inside the edges of the screen
289
+ * return the photo to line up with the edges, otherwise use decay
290
+ * with a clamping at the edges to give the effect the image is
291
+ * sliding along using velocity and friction
292
+ */
293
+ translateX.value =
294
+ scale.value < 1
295
+ ? withTiming(0)
296
+ : translateX.value > halfScreenWidth * (scale.value - 1)
297
+ ? withTiming(halfScreenWidth * (scale.value - 1), {
298
+ duration: 200,
299
+ })
300
+ : translateX.value < -halfScreenWidth * (scale.value - 1)
301
+ ? withTiming(-halfScreenWidth * (scale.value - 1), {
302
+ duration: 200,
303
+ })
304
+ : withDecay({
305
+ clamp: [
306
+ -halfScreenWidth * (scale.value - 1),
307
+ halfScreenWidth * (scale.value - 1),
308
+ ],
309
+ deceleration: 0.99,
310
+ velocity: -evt.velocityX,
311
+ });
312
+
313
+ /**
314
+ * When the pan is finished if the height is less than the screen
315
+ * height return the photo to center, if the photo is inside the
316
+ * edges of the screen return the photo to line up with the edges,
317
+ * otherwise use decay with a clamping at the edges to give the effect
318
+ * the image is sliding along using velocity and friction
319
+ */
320
+ translateY.value =
321
+ currentImageHeight * scale.value < screenHeight
322
+ ? withTiming(0)
323
+ : translateY.value > (currentImageHeight / 2) * scale.value - halfScreenHeight
324
+ ? withTiming((currentImageHeight / 2) * scale.value - halfScreenHeight)
325
+ : translateY.value < (-currentImageHeight / 2) * scale.value + halfScreenHeight
326
+ ? withTiming((-currentImageHeight / 2) * scale.value + halfScreenHeight)
327
+ : withDecay({
328
+ clamp: [
329
+ (-currentImageHeight / 2) * scale.value + halfScreenHeight,
330
+ (currentImageHeight / 2) * scale.value - halfScreenHeight,
331
+ ],
332
+ deceleration: 0.99,
333
+ velocity: evt.velocityY,
334
+ });
335
+
336
+ resetTouchValues();
337
+
338
+ /**
339
+ * If the scale has been reduced below one, i.e. zoomed out, translate
340
+ * the zoom back to one
341
+ */
342
+ scale.value =
343
+ scale.value !== offsetScale.value ? withTiming(offsetScale.value) : offsetScale.value;
344
+
345
+ /**
346
+ * If the photo is centered or at the top of the screen if scaled larger
347
+ * than the screen, and not paging left or right, and the final Y position
348
+ * is greater than half the screen using swipe velocity and position, close
349
+ * the overlay
350
+ */
351
+ if (
352
+ finalYPosition > halfScreenHeight &&
353
+ offsetY.value + 8 >= (currentImageHeight / 2) * scale.value - halfScreenHeight &&
354
+ isSwiping.value !== IsSwiping.TRUE &&
355
+ translateY.value !== 0 &&
356
+ !(
357
+ Math.abs(halfScreenWidth * (scale.value - 1) + offsetX.value) < 3 &&
358
+ translateX.value < 0 &&
359
+ finalXPosition < -halfScreenWidth
360
+ ) &&
361
+ !(
362
+ Math.abs(-halfScreenWidth * (scale.value - 1) + offsetX.value) < 3 &&
363
+ translateX.value > 0 &&
364
+ finalXPosition > halfScreenWidth
365
+ )
366
+ ) {
367
+ cancelAnimation(translateX);
368
+ cancelAnimation(translateY);
369
+ cancelAnimation(scale);
370
+ overlayOpacity.value = withTiming(
371
+ 0,
372
+ {
373
+ duration: 200,
374
+ easing: Easing.out(Easing.ease),
375
+ },
376
+ () => {
377
+ runOnJS(setOverlay)('none');
378
+ },
379
+ );
380
+ scale.value = withTiming(0.6, {
381
+ duration: 200,
382
+ easing: Easing.out(Easing.ease),
383
+ });
384
+ translateY.value =
385
+ evt.velocityY > 1000
386
+ ? withDecay({
387
+ velocity: evt.velocityY,
388
+ })
389
+ : withTiming(halfScreenHeight + (currentImageHeight / 2) * scale.value, {
390
+ duration: 200,
391
+ easing: Easing.out(Easing.ease),
392
+ });
393
+ translateX.value = withDecay({
394
+ velocity: -evt.velocityX,
395
+ });
396
+ }
397
+ }
398
+ },
399
+ onStart: () => {
400
+ if (!isPinch.value) {
401
+ /**
402
+ * Cancel any previous motion animation on translations when a touch
403
+ * begins to interrupt the animation and take over the position handling
404
+ */
405
+ cancelAnimation(translateX);
406
+ cancelAnimation(translateY);
407
+ cancelAnimation(scale);
408
+ offsetX.value = translateX.value;
409
+ offsetY.value = translateY.value;
410
+ }
411
+
412
+ /**
413
+ * Reset hasPinched for Android single finger offset
414
+ */
415
+ hasPinched.value = HasPinched.FALSE;
416
+ },
417
+ },
418
+ [currentImageHeight, photoLength],
419
+ );
420
+
421
+ /**
422
+ * On pinch is run when two or more fingers touch the screen, it then takes over
423
+ * all touch handling even if the number of fingers is reduced to one until the
424
+ * touch is complete
425
+ */
426
+ const onPinch = useAnimatedGestureHandler<PinchGestureHandlerGestureEvent>(
427
+ {
428
+ onActive: (evt) => {
429
+ /**
430
+ * Android starts with a zero event with 1 touch instead of two
431
+ * we therefore must wait to capture starting info until the double
432
+ * touch begins
433
+ */
434
+ if (!isPinch.value && isAndroid) {
435
+ /**
436
+ * Set hasPinched to true so when removing one finger the pan active
437
+ * state adjusts the offset
438
+ */
439
+ hasPinched.value = HasPinched.TRUE;
440
+
441
+ /**
442
+ * Cancel any previous motion animation on translations when a touch
443
+ * begins to interrupt the animation and take over the position handling
444
+ */
445
+ cancelAnimation(translateX);
446
+ cancelAnimation(translateY);
447
+ cancelAnimation(scale);
448
+
449
+ /**
450
+ * Reset isSwiping as now the pan gesture handler is no longer running
451
+ */
452
+ isSwiping.value = IsSwiping.UNDETERMINED;
453
+
454
+ /**
455
+ * Set initial values for pinch gesture interaction handler
456
+ */
457
+ numberOfPinchFingers.value = evt.numberOfPointers;
458
+ offsetX.value = translateX.value;
459
+ offsetY.value = translateY.value;
460
+ adjustedFocalX.value = evt.focalX - (halfScreenWidth - offsetX.value);
461
+ adjustedFocalY.value = evt.focalY - (halfScreenHeight + offsetY.value);
462
+ originX.value = adjustedFocalX.value;
463
+ originY.value = adjustedFocalY.value;
464
+ offsetScale.value = scale.value;
465
+ }
466
+
467
+ /**
468
+ * Set pinch to true to stop all pan gesture interactions, we do this
469
+ * again here for Android outside the check that creates type
470
+ */
471
+ isPinch.value = true;
472
+
473
+ /**
474
+ * The scale is clamped to a minimum of 1 and maximum of 8 for aesthetics.
475
+ * We use the clamped value to determine a local event scale so the focal
476
+ * point does not become out of sync with the actual photo scaling, e.g.
477
+ * evt.scale is 20 but scale is 8, using evt.scale for offset will put the
478
+ * photo and calculations out of sync
479
+ */
480
+ scale.value = clamp(offsetScale.value * evt.scale, 1, 8);
481
+ const localEvtScale = scale.value / offsetScale.value;
482
+
483
+ /**
484
+ * When we hit the top or bottom of the scale clamping we run a haptic
485
+ * trigger, we track if it has been run to not spam the trigger
486
+ */
487
+ if (scale.value !== 8 && scale.value !== 1) {
488
+ hasHitTopScale.value = 0;
489
+ hasHitBottomScale.value = 0;
490
+ } else if (scale.value === 8 && hasHitTopScale.value === 0) {
491
+ hasHitTopScale.value = 1;
492
+ runOnJS(triggerHaptic)('impactLight');
493
+ } else if (scale.value === 1 && hasHitBottomScale.value === 0) {
494
+ hasHitBottomScale.value = 1;
495
+ runOnJS(triggerHaptic)('impactLight');
496
+ }
497
+
498
+ /**
499
+ * We calculate the adjusted focal point on the photo using the events
500
+ * focal position on the screen, screen size, and current photo offset
501
+ */
502
+ adjustedFocalX.value = evt.focalX - (halfScreenWidth - offsetX.value);
503
+ adjustedFocalY.value = evt.focalY - (halfScreenHeight + offsetY.value);
504
+
505
+ /**
506
+ * If the number of fingers on the screen changes, the position of the
507
+ * focal point will change and this needs to be accounted for, e.g. if
508
+ * two fingers are on the screen the focal is between them, but if one is
509
+ * then removed the focal is now at the remaining fingers touch position.
510
+ * If this happens without accounting for the change the image will jump
511
+ * around, we keep track of the previous two finger focal to adjust for this
512
+ * change in a reduction from two fingers to one, then if another finger
513
+ * is added again we adjust the origin to account for the difference between
514
+ * the original two finger touch and the new two finger touch position.
515
+ */
516
+ if (numberOfPinchFingers.value !== evt.numberOfPointers) {
517
+ numberOfPinchFingers.value = evt.numberOfPointers;
518
+ if (evt.numberOfPointers === 1) {
519
+ focalOffsetX.value = oldFocalX.value - adjustedFocalX.value;
520
+ focalOffsetY.value = oldFocalY.value - adjustedFocalY.value;
521
+ } else if (numberOfPinchFingers.value > 1) {
522
+ originX.value =
523
+ originX.value -
524
+ (oldFocalX.value / localEvtScale - adjustedFocalX.value / localEvtScale);
525
+ originY.value =
526
+ originY.value -
527
+ (oldFocalY.value / localEvtScale - adjustedFocalY.value / localEvtScale);
528
+ }
529
+ }
530
+
531
+ /**
532
+ * If pinch handler has been activated via two fingers then the fingers
533
+ * reduced to one we keep track of the old focal using the focal offset
534
+ * from when the number of fingers was two. We then translate the photo
535
+ * taking into account the offset, focal, focal offset, origin, and scale.
536
+ */
537
+ if (numberOfPinchFingers.value === 1) {
538
+ oldFocalX.value = adjustedFocalX.value + focalOffsetX.value;
539
+ oldFocalY.value = adjustedFocalY.value + focalOffsetY.value;
540
+ translateX.value = offsetX.value - oldFocalX.value + localEvtScale * originX.value;
541
+ translateY.value = offsetY.value + oldFocalY.value - localEvtScale * originY.value;
542
+
543
+ /**
544
+ * If the number of fingers in the gesture is greater than one the
545
+ * adjusted focal point is saved as the old focal and the photo is
546
+ * translated taking into account the offset, focal, origin, and scale.
547
+ */
548
+ } else if (numberOfPinchFingers.value > 1) {
549
+ oldFocalX.value = adjustedFocalX.value;
550
+ oldFocalY.value = adjustedFocalY.value;
551
+ translateX.value = offsetX.value - adjustedFocalX.value + localEvtScale * originX.value;
552
+ translateY.value = offsetY.value + adjustedFocalY.value - localEvtScale * originY.value;
553
+ }
554
+ },
555
+ onFinish: () => {
556
+ if (isPinch.value) {
557
+ /**
558
+ * When the pinch is finished if the scale is less than 1 return the
559
+ * photo to center, if the photo is inside the edges of the screen
560
+ * return the photo to line up with the edges, otherwise leave the
561
+ * photo in its current position
562
+ */
563
+ translateX.value =
564
+ scale.value < 1
565
+ ? withTiming(0)
566
+ : translateX.value > halfScreenWidth * (scale.value - 1)
567
+ ? withTiming(halfScreenWidth * (scale.value - 1))
568
+ : translateX.value < -halfScreenWidth * (scale.value - 1)
569
+ ? withTiming(-halfScreenWidth * (scale.value - 1))
570
+ : translateX.value;
571
+
572
+ /**
573
+ * When the pinch is finished if the height is less than the screen
574
+ * height return the photo to center, if the photo is inside the
575
+ * edges of the screen return the photo to line up with the edges,
576
+ * otherwise leave the photo in its current position
577
+ */
578
+ translateY.value =
579
+ currentImageHeight * scale.value < screenHeight
580
+ ? withTiming(0)
581
+ : translateY.value > (currentImageHeight / 2) * scale.value - screenHeight / 2
582
+ ? withTiming((currentImageHeight / 2) * scale.value - screenHeight / 2)
583
+ : translateY.value < (-currentImageHeight / 2) * scale.value + screenHeight / 2
584
+ ? withTiming((-currentImageHeight / 2) * scale.value + screenHeight / 2)
585
+ : translateY.value;
586
+
587
+ /**
588
+ * If the scale has been reduced below one, i.e. zoomed out, translate
589
+ * the zoom back to one
590
+ */
591
+ offsetScale.value = scale.value < 1 ? 1 : scale.value;
592
+ scale.value = scale.value < 1 ? withTiming(1) : scale.value;
593
+ resetTouchValues();
594
+ }
595
+ },
596
+ onStart: (evt) => {
597
+ /**
598
+ * Android starts with a zero event with 1 touch instead of two
599
+ * we therefore must wait to capture starting info until the double
600
+ * touch begins
601
+ */
602
+ if (!isAndroid) {
603
+ /**
604
+ * Cancel any previous motion animation on translations when a touch
605
+ * begins to interrupt the animation and take over the position handling
606
+ */
607
+ cancelAnimation(translateX);
608
+ cancelAnimation(translateY);
609
+ cancelAnimation(scale);
610
+
611
+ /**
612
+ * Set pinch to true to stop all pan gesture interactions
613
+ */
614
+ isPinch.value = true;
615
+
616
+ /**
617
+ * Reset isSwiping as now the pan gesture handler is no longer running
618
+ */
619
+ isSwiping.value = IsSwiping.UNDETERMINED;
620
+
621
+ /**
622
+ * Set initial values for pinch gesture interaction handler
623
+ */
624
+ numberOfPinchFingers.value = evt.numberOfPointers;
625
+ offsetX.value = translateX.value;
626
+ offsetY.value = translateY.value;
627
+ adjustedFocalX.value = evt.focalX - (halfScreenWidth - offsetX.value);
628
+ adjustedFocalY.value = evt.focalY - (halfScreenHeight + offsetY.value);
629
+ originX.value = adjustedFocalX.value;
630
+ originY.value = adjustedFocalY.value;
631
+ offsetScale.value = scale.value;
632
+ }
633
+
634
+ /**
635
+ * Reset hasPinched for Android single finger offset
636
+ */
637
+ hasPinched.value = HasPinched.FALSE;
638
+ },
639
+ },
640
+ [currentImageHeight],
641
+ );
642
+
643
+ /**
644
+ * Single tap handler for header hiding and showing
645
+ */
646
+ const onSingleTap = useAnimatedGestureHandler<TapGestureHandlerGestureEvent>({
647
+ onActive: () => {
648
+ cancelAnimation(headerFooterVisible);
649
+ headerFooterVisible.value = headerFooterVisible.value > 0 ? withTiming(0) : withTiming(1);
650
+ },
651
+ });
652
+
653
+ /**
654
+ * Double tap handler to zoom back out and hide header and footer
655
+ */
656
+ const onDoubleTap = useAnimatedGestureHandler<TapGestureHandlerGestureEvent>({
657
+ onActive: (evt) => {
658
+ if (Math.abs(tapX.value - evt.absoluteX) < 64 && Math.abs(tapY.value - evt.absoluteY) < 64) {
659
+ if (offsetScale.value === 1 && offsetX.value === 0 && offsetY.value === 0) {
660
+ offsetScale.value = 2;
661
+ scale.value = withTiming(2, {
662
+ duration: 200,
663
+ easing: Easing.out(Easing.ease),
664
+ });
665
+ translateX.value = withTiming(evt.absoluteX - halfScreenWidth, {
666
+ duration: 200,
667
+ easing: Easing.out(Easing.ease),
668
+ });
669
+ if (currentImageHeight * 2 > screenHeight) {
670
+ const translateYTopBottom =
671
+ evt.absoluteY > halfScreenHeight
672
+ ? -(currentImageHeight * 2 - screenHeight) / 2
673
+ : (currentImageHeight * 2 - screenHeight) / 2;
674
+ translateY.value = withTiming(translateYTopBottom, {
675
+ duration: 200,
676
+ easing: Easing.out(Easing.ease),
677
+ });
678
+ }
679
+ } else {
680
+ offsetScale.value = 1;
681
+ scale.value = withTiming(1, {
682
+ duration: 200,
683
+ easing: Easing.out(Easing.ease),
684
+ });
685
+ offsetX.value = 0;
686
+ offsetY.value = 0;
687
+ translateX.value = withTiming(0, {
688
+ duration: 200,
689
+ easing: Easing.out(Easing.ease),
690
+ });
691
+ translateY.value = withTiming(0, {
692
+ duration: 200,
693
+ easing: Easing.out(Easing.ease),
694
+ });
695
+ if (headerFooterVisible.value !== 0) {
696
+ cancelAnimation(headerFooterVisible);
697
+ headerFooterVisible.value = withTiming(0);
698
+ }
699
+ }
700
+ }
701
+ },
702
+ onStart: (evt) => {
703
+ tapX.value = evt.absoluteX;
704
+ tapY.value = evt.absoluteY;
705
+ },
706
+ });
707
+
708
+ return {
709
+ onDoubleTap,
710
+ onPan,
711
+ onPinch,
712
+ onSingleTap,
713
+ };
714
+ };
715
+
716
+ /**
717
+ * Clamping worklet to clamp the scaling
718
+ */
719
+ export const clamp = (value: number, lowerBound: number, upperBound: number) => {
720
+ 'worklet';
721
+ return Math.min(Math.max(lowerBound, value), upperBound);
722
+ };