@planningcenter/chat-react-native 3.12.0-rc.1 → 3.12.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,8 +4,10 @@ export type MetaProps = {
4
4
  authorName: string;
5
5
  createdAt: string;
6
6
  };
7
- export declare function ImageAttachment({ attachment, metaProps, onMessageAttachmentLongPress, }: {
7
+ export declare function ImageAttachment({ attachment, imageAttachments, currentImageIndex, metaProps, onMessageAttachmentLongPress, }: {
8
8
  attachment: DenormalizedMessageAttachmentResource;
9
+ imageAttachments: DenormalizedMessageAttachmentResource[];
10
+ currentImageIndex: number;
9
11
  metaProps: MetaProps;
10
12
  onMessageAttachmentLongPress: (attachment: DenormalizedMessageAttachmentResource) => void;
11
13
  }): React.JSX.Element;
@@ -1 +1 @@
1
- {"version":3,"file":"image_attachment.d.ts","sourceRoot":"","sources":["../../../../src/components/conversation/attachments/image_attachment.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAsD,MAAM,OAAO,CAAA;AAqB1E,OAAO,EAAE,qCAAqC,EAAE,MAAM,2DAA2D,CAAA;AAoBjH,MAAM,MAAM,SAAS,GAAG;IACtB,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;CAClB,CAAA;AAED,wBAAgB,eAAe,CAAC,EAC9B,UAAU,EACV,SAAS,EACT,4BAA4B,GAC7B,EAAE;IACD,UAAU,EAAE,qCAAqC,CAAA;IACjD,SAAS,EAAE,SAAS,CAAA;IACpB,4BAA4B,EAAE,CAAC,UAAU,EAAE,qCAAqC,KAAK,IAAI,CAAA;CAC1F,qBAkCA"}
1
+ {"version":3,"file":"image_attachment.d.ts","sourceRoot":"","sources":["../../../../src/components/conversation/attachments/image_attachment.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmE,MAAM,OAAO,CAAA;AAkCvF,OAAO,EAAE,qCAAqC,EAAE,MAAM,2DAA2D,CAAA;AAsBjH,MAAM,MAAM,SAAS,GAAG;IACtB,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;CAClB,CAAA;AAED,wBAAgB,eAAe,CAAC,EAC9B,UAAU,EACV,gBAAgB,EAChB,iBAAiB,EACjB,SAAS,EACT,4BAA4B,GAC7B,EAAE;IACD,UAAU,EAAE,qCAAqC,CAAA;IACjD,gBAAgB,EAAE,qCAAqC,EAAE,CAAA;IACzD,iBAAiB,EAAE,MAAM,CAAA;IACzB,SAAS,EAAE,SAAS,CAAA;IACpB,4BAA4B,EAAE,CAAC,UAAU,EAAE,qCAAqC,KAAK,IAAI,CAAA;CAC1F,qBAiCA"}
@@ -1,7 +1,7 @@
1
- import React, { useMemo, useState } from 'react';
2
- import { StatusBar, StyleSheet, Modal, View, Linking, Dimensions, Platform } from 'react-native';
1
+ import React, { useMemo, useState, useCallback } from 'react';
2
+ import { StatusBar, StyleSheet, Modal, View, Linking, Dimensions, Platform, } from 'react-native';
3
3
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
4
- import { Gesture, GestureDetector, GestureHandlerRootView, Pressable, } from 'react-native-gesture-handler';
4
+ import { FlatList, Gesture, GestureDetector, GestureHandlerRootView, Pressable, } from 'react-native-gesture-handler';
5
5
  import Animated, { runOnJS, useAnimatedStyle, useAnimatedReaction, useSharedValue, withSpring, withDecay, } from 'react-native-reanimated';
6
6
  import { tokens } from '../../../vendor/tapestry/tokens';
7
7
  import { IconButton, Image, Heading, Text } from '../../display';
@@ -14,6 +14,8 @@ const DISMISS_PAN_THRESHOLD = 300;
14
14
  const MIN_DISTANCE_PAN_FOR_PLATFORM = Platform.OS === 'android' ? 5 : 0; // Android requires a higher threshold to give pinching priority
15
15
  const POINTER_FOR_SINGLE_FINGER_PAN = 1; // Single-finger panning helps to avoid conflicts with pinching
16
16
  const DEFAULT_OPACITY = 1;
17
+ const DEFAULT_TRANSLATE_X = 0;
18
+ const DEFAULT_TRANSLATE_Y = 0;
17
19
  const DEFAULT_SCALE = 1;
18
20
  const MIN_SCALE = 0.5;
19
21
  const MAX_SCALE = 5;
@@ -24,7 +26,7 @@ const RESET_SPRING_CONFIG = {
24
26
  damping: 20,
25
27
  stiffness: 150,
26
28
  };
27
- export function ImageAttachment({ attachment, metaProps, onMessageAttachmentLongPress, }) {
29
+ export function ImageAttachment({ attachment, imageAttachments, currentImageIndex, metaProps, onMessageAttachmentLongPress, }) {
28
30
  const { attributes } = attachment;
29
31
  const { url, urlMedium, filename, metadata = {} } = attributes;
30
32
  const { colors } = useTheme();
@@ -32,15 +34,21 @@ export function ImageAttachment({ attachment, metaProps, onMessageAttachmentLong
32
34
  const [visible, setVisible] = useState(false);
33
35
  return (<>
34
36
  <PlatformPressable style={styles.container} onPress={() => setVisible(true)} onLongPress={() => onMessageAttachmentLongPress(attachment)} android_ripple={{ color: colors.androidRippleNeutral, foreground: true }} accessibilityHint="Long press for more options">
35
- <Image source={{ uri: urlMedium || url }} style={styles.image} wrapperStyle={styles.imageWrapper} alt={filename}/>
37
+ <Image source={{ uri: urlMedium || url }} style={styles.image} wrapperStyle={styles.attachmentImageWrapper} alt={filename}/>
36
38
  </PlatformPressable>
37
- <LightboxModal visible={visible} setModalVisible={setVisible} uri={urlMedium || url} metaProps={metaProps} imageWidth={metadata.width} imageHeight={metadata.height}/>
39
+ <LightboxModal visible={visible} setModalVisible={setVisible} imageAttachments={imageAttachments} initialImageIndex={currentImageIndex} metaProps={metaProps}/>
38
40
  </>);
39
41
  }
40
- const LightboxModal = ({ uri, visible, setModalVisible, metaProps, imageWidth, imageHeight, }) => {
42
+ const LightboxModal = ({ visible, setModalVisible, imageAttachments, initialImageIndex, metaProps, }) => {
41
43
  const styles = useStyles();
42
44
  const insets = useSafeAreaInsets();
45
+ const [currentImageIndex, setCurrentImageIndex] = useState(initialImageIndex);
43
46
  const { authorName, createdAt } = metaProps;
47
+ // Get current image data
48
+ const currentImage = imageAttachments[currentImageIndex];
49
+ const { url, urlMedium, metadata = {} } = currentImage.attributes;
50
+ const imageWidth = metadata.width;
51
+ const imageHeight = metadata.height;
44
52
  // Calculate available space for image display
45
53
  const availableWindowWidth = WINDOW_WIDTH;
46
54
  const availableWindowHeight = WINDOW_HEIGHT - insets.top - insets.bottom;
@@ -53,45 +61,64 @@ const LightboxModal = ({ uri, visible, setModalVisible, metaProps, imageWidth, i
53
61
  const scale = useSharedValue(DEFAULT_SCALE); // zoom level of image
54
62
  const focalX = useSharedValue(0); // focal point of image between fingers
55
63
  const focalY = useSharedValue(0); // focal point of image between fingers
56
- const translateX = useSharedValue(0); // horizontal distance to pan image
57
- const translateY = useSharedValue(0); // vertical distance to pan image
64
+ const translateX = useSharedValue(DEFAULT_TRANSLATE_X); // horizontal distance to pan image
65
+ const translateY = useSharedValue(DEFAULT_TRANSLATE_Y); // vertical distance to pan image
58
66
  const savedScale = useSharedValue(DEFAULT_SCALE); // previous zoom level
59
- const savedTranslateX = useSharedValue(0); // previous horizontal position
60
- const savedTranslateY = useSharedValue(0); // previous vertical position
67
+ const savedTranslateX = useSharedValue(DEFAULT_TRANSLATE_X); // previous horizontal position
68
+ const savedTranslateY = useSharedValue(DEFAULT_TRANSLATE_Y); // previous vertical position
61
69
  const toolbarVisible = useSharedValue(1); // toolbar visibility state
62
70
  // React (JS) State:
63
71
  const [isStatusBarHidden, setIsStatusBarHidden] = useState(false);
72
+ const [flatListScrollEnabled, setFlatListScrollEnabled] = useState(true);
73
+ const [panGestureEnabled, setPanGestureEnabled] = useState(false);
64
74
  // Syncs toolbar useSharedValue state with React's state so that the status bar can be hidden/shown based on the toolbar's animation
65
75
  useAnimatedReaction(() => toolbarVisible.value, value => {
66
76
  runOnJS(setIsStatusBarHidden)(value === 0);
67
77
  });
78
+ // Syncs FlatList scroll state with scale changes
79
+ // When image is at default scale, enable scroll and disable pan gesture
80
+ // When image is zoomed in, disable scroll and enable pan gesture
81
+ useAnimatedReaction(() => scale.value, value => {
82
+ const enableFlatListScroll = value === DEFAULT_SCALE;
83
+ runOnJS(setFlatListScrollEnabled)(enableFlatListScroll);
84
+ runOnJS(setPanGestureEnabled)(!enableFlatListScroll);
85
+ });
68
86
  /* ============================
69
87
  HANDLERS
70
88
  ============================ */
71
- const handleOpenInBrowser = () => {
72
- Linking.openURL(uri);
73
- };
74
- const resetDismissGestures = () => {
89
+ const handleOpenInBrowser = useCallback(() => {
90
+ Linking.openURL(urlMedium || url);
91
+ }, [urlMedium, url]);
92
+ const resetDismissGestures = useCallback(() => {
75
93
  dismissY.value = withSpring(0, RESET_SPRING_CONFIG);
76
94
  opacity.value = withSpring(DEFAULT_OPACITY, RESET_SPRING_CONFIG);
77
- };
78
- const resetAllGestures = () => {
95
+ }, [dismissY, opacity]);
96
+ const resetAllGestures = useCallback(() => {
79
97
  resetDismissGestures();
80
98
  scale.value = withSpring(DEFAULT_SCALE, RESET_SPRING_CONFIG);
81
- translateX.value = withSpring(0, RESET_SPRING_CONFIG);
82
- translateY.value = withSpring(0, RESET_SPRING_CONFIG);
99
+ translateX.value = withSpring(DEFAULT_TRANSLATE_X, RESET_SPRING_CONFIG);
100
+ translateY.value = withSpring(DEFAULT_TRANSLATE_Y, RESET_SPRING_CONFIG);
83
101
  savedScale.value = DEFAULT_SCALE;
84
- savedTranslateX.value = 0;
85
- savedTranslateY.value = 0;
102
+ savedTranslateX.value = DEFAULT_TRANSLATE_X;
103
+ savedTranslateY.value = DEFAULT_TRANSLATE_Y;
86
104
  toolbarVisible.value = withSpring(1, RESET_SPRING_CONFIG);
87
- };
88
- const handleCloseModal = () => {
105
+ }, [
106
+ resetDismissGestures,
107
+ scale,
108
+ translateX,
109
+ translateY,
110
+ savedScale,
111
+ savedTranslateX,
112
+ savedTranslateY,
113
+ toolbarVisible,
114
+ ]);
115
+ const handleCloseModal = useCallback(() => {
89
116
  setModalVisible(false);
90
117
  resetAllGestures();
91
- };
118
+ }, [setModalVisible, resetAllGestures]);
92
119
  /* ============================
93
- UTILITY FUNCTIONS
94
- 'worklet' runs functions on the UI thread, instead of the JS thread for better performance
120
+ UTILITY WORKLET FUNCTIONS
121
+ 'worklet' runs functions on the UI thread, instead of the JS thread for better performance.
95
122
  ============================ */
96
123
  const getImageContainedToWindowDimensions = () => {
97
124
  'worklet';
@@ -140,8 +167,8 @@ const LightboxModal = ({ uri, visible, setModalVisible, metaProps, imageWidth, i
140
167
  const excessWidth = scaledWidth - availableWindowWidth;
141
168
  const excessHeight = scaledHeight - availableWindowHeight;
142
169
  // How far the image can move in each direction before hitting window edges
143
- const maxTranslateX = Math.max(0, excessWidth / 2);
144
- const maxTranslateY = Math.max(0, excessHeight / 2);
170
+ const maxTranslateX = Math.max(DEFAULT_TRANSLATE_X, excessWidth / 2);
171
+ const maxTranslateY = Math.max(DEFAULT_TRANSLATE_Y, excessHeight / 2);
145
172
  return { maxTranslateX, maxTranslateY };
146
173
  };
147
174
  const clampDecay = (currentScale) => {
@@ -166,13 +193,48 @@ const LightboxModal = ({ uri, visible, setModalVisible, metaProps, imageWidth, i
166
193
  // Calculate how much the image can exceed the window container
167
194
  const scaledHeight = containedImageHeight * currentScale;
168
195
  const excessHeight = scaledHeight - availableWindowHeight;
169
- const maxTranslateY = Math.max(0, excessHeight / 2);
196
+ const maxTranslateY = Math.max(DEFAULT_TRANSLATE_Y, excessHeight / 2);
170
197
  const currentTranslateY = translateY.value;
171
198
  const panPositionTolerance = 1; // buffer to account for translateY being at a subpixel position
172
199
  const atTopBoundry = currentTranslateY >= maxTranslateY - panPositionTolerance;
173
200
  const atBottomBoundry = currentTranslateY <= -maxTranslateY + panPositionTolerance;
174
201
  return atTopBoundry || atBottomBoundry;
175
202
  };
203
+ /* ============================
204
+ UTILITY FLATLIST FUNCTIONS
205
+ Supports the image gallery layout and swipe functionality.
206
+ ============================ */
207
+ // Used in tandem with FlatList's initialScrollIndex to quickly calculate the position and size of each image before they load.
208
+ const getItemLayout = useCallback((_, index) => ({
209
+ length: WINDOW_WIDTH,
210
+ offset: WINDOW_WIDTH * index,
211
+ index,
212
+ }), []);
213
+ // Captures the current image's index after the FlatList finishes its scroll animation.
214
+ // Used in tandem with onViewableItemsChanged to ensure the final value for currentImageIndex is set.
215
+ const onMomentumScrollEnd = useCallback((event) => {
216
+ // Calculate the index of the image that is currently visible
217
+ const imageOffsetX = event.nativeEvent.contentOffset.x;
218
+ const newImageIndex = Math.round(imageOffsetX / WINDOW_WIDTH);
219
+ // Check if the image index has changed and the FlatList didn't scroll past the first or last image
220
+ const didImageIndexChange = newImageIndex !== currentImageIndex;
221
+ const isImageIndexWithinBounds = newImageIndex >= 0 && newImageIndex < imageAttachments.length;
222
+ if (didImageIndexChange && isImageIndexWithinBounds) {
223
+ setCurrentImageIndex(newImageIndex);
224
+ }
225
+ }, [currentImageIndex, imageAttachments.length]);
226
+ // Supplements onMomentumScrollEnd by capturing the current image's index while the FlatList is actively scrolling.
227
+ // Used in tandem with viewabilityConfig to trigger when the image is 50% visible in the window.
228
+ const onViewableItemsChanged = useCallback(({ viewableItems }) => {
229
+ if (viewableItems.length === 0)
230
+ return;
231
+ // Use the first viewable item which is enforced by the FlatList's pagingEnabled prop that allows only two images to be visible at a time when scrolling.
232
+ const firstViewableItem = viewableItems[0];
233
+ const newIndex = firstViewableItem.index;
234
+ if (newIndex !== null && newIndex !== currentImageIndex) {
235
+ setCurrentImageIndex(newIndex);
236
+ }
237
+ }, [currentImageIndex]);
176
238
  /* ============================
177
239
  GESTURES
178
240
  ============================ */
@@ -287,6 +349,7 @@ const LightboxModal = ({ uri, visible, setModalVisible, metaProps, imageWidth, i
287
349
  .minDistance(MIN_DISTANCE_PAN_FOR_PLATFORM)
288
350
  .minPointers(POINTER_FOR_SINGLE_FINGER_PAN)
289
351
  .maxPointers(POINTER_FOR_SINGLE_FINGER_PAN)
352
+ .enabled(panGestureEnabled)
290
353
  .onStart(() => {
291
354
  // Update saved position to current animated position
292
355
  // This ensures smooth continuation if decay is running
@@ -366,14 +429,6 @@ const LightboxModal = ({ uri, visible, setModalVisible, metaProps, imageWidth, i
366
429
  const transformImageGestures = Gesture.Race(tapGestures, pinchOrPanGestures);
367
430
  // Dismiss can work simultaneously with all gestures
368
431
  const composedGesture = Gesture.Simultaneous(transformImageGestures, panToDismissModalGesture);
369
- const animatedImageStyles = useAnimatedStyle(() => ({
370
- transform: [
371
- { translateX: translateX.value },
372
- { translateY: translateY.value + dismissY.value },
373
- { scale: scale.value },
374
- ],
375
- opacity: opacity.value,
376
- }));
377
432
  const animatedToolbarStyles = useAnimatedStyle(() => ({
378
433
  opacity: toolbarVisible.value,
379
434
  transform: [
@@ -382,15 +437,16 @@ const LightboxModal = ({ uri, visible, setModalVisible, metaProps, imageWidth, i
382
437
  }));
383
438
  return (<Modal visible={visible} transparent animationType="fade" onRequestClose={handleCloseModal}>
384
439
  <StatusBar barStyle="light-content" hidden={isStatusBarHidden} animated showHideTransition="slide"/>
385
- <View style={styles.lightboxModalSafeArea}>
386
- <GestureHandlerRootView>
387
- <GestureDetector gesture={composedGesture}>
388
- <PreventPressEventsBubbling>
389
- <Image source={{ uri }} loadingBackgroundStyles={styles.lightboxImageLoading} style={styles.lightboxImage} animatedImageStyle={animatedImageStyles} resizeMode="contain" animated={true} alt=""/>
390
- </PreventPressEventsBubbling>
391
- </GestureDetector>
392
- </GestureHandlerRootView>
393
- </View>
440
+ <GestureHandlerRootView>
441
+ <GestureDetector gesture={composedGesture}>
442
+ <PreventPressEventsBubbling>
443
+ <FlatList data={imageAttachments} renderItem={({ item, index }) => (<GestureImage item={item} gesturesEnabled={index === currentImageIndex} scale={scale} translateX={translateX} translateY={translateY} dismissY={dismissY} opacity={opacity}/>)} keyExtractor={(item, index) => `${item.id}-${index}`} horizontal pagingEnabled scrollEnabled={flatListScrollEnabled} showsHorizontalScrollIndicator={false} initialScrollIndex={initialImageIndex} getItemLayout={getItemLayout} onMomentumScrollEnd={onMomentumScrollEnd} onViewableItemsChanged={onViewableItemsChanged} viewabilityConfig={{
444
+ itemVisiblePercentThreshold: 50, // 50% of the image must be visible in the window to be considered viewable
445
+ }} style={styles.gallery} contentContainerStyle={styles.galleryContentContainer}/>
446
+ </PreventPressEventsBubbling>
447
+ </GestureDetector>
448
+ </GestureHandlerRootView>
449
+
394
450
  <Animated.View style={[styles.actionToolbar, animatedToolbarStyles]} accessibilityRole="toolbar">
395
451
  <View style={styles.actionToolbarTextMeta}>
396
452
  <Heading variant="h3" style={styles.actionToolbarTitle} numberOfLines={1}>
@@ -410,6 +466,21 @@ const PreventPressEventsBubbling = ({ children }) => {
410
466
  {children}
411
467
  </Pressable>);
412
468
  };
469
+ const GestureImage = ({ item, gesturesEnabled, scale, translateX, translateY, dismissY, opacity, }) => {
470
+ const styles = useStyles();
471
+ const { url: itemUrl, urlMedium: itemUrlMedium } = item.attributes;
472
+ const animatedImageStyles = useAnimatedStyle(() => {
473
+ return {
474
+ transform: [
475
+ { translateX: gesturesEnabled ? translateX.value : DEFAULT_TRANSLATE_X },
476
+ { translateY: gesturesEnabled ? translateY.value + dismissY.value : DEFAULT_TRANSLATE_Y },
477
+ { scale: gesturesEnabled ? scale.value : DEFAULT_SCALE },
478
+ ],
479
+ opacity: opacity.value,
480
+ };
481
+ });
482
+ return (<Image source={{ uri: itemUrlMedium || itemUrl }} style={styles.gestureImage} animatedImageStyle={animatedImageStyles} loadingBackgroundStyles={styles.gestureImageLoading} resizeMode="contain" alt=""/>);
483
+ };
413
484
  const useStyles = ({ imageWidth = 100, imageHeight = 100 } = {}) => {
414
485
  const { top, bottom } = useSafeAreaInsets();
415
486
  const backgroundColor = tokens.colorNeutral7;
@@ -418,7 +489,7 @@ const useStyles = ({ imageWidth = 100, imageHeight = 100 } = {}) => {
418
489
  container: {
419
490
  maxWidth: '100%',
420
491
  },
421
- imageWrapper: {
492
+ attachmentImageWrapper: {
422
493
  width: '100%',
423
494
  minWidth: 200,
424
495
  aspectRatio: imageWidth / imageHeight,
@@ -426,20 +497,19 @@ const useStyles = ({ imageWidth = 100, imageHeight = 100 } = {}) => {
426
497
  image: {
427
498
  borderRadius: 8,
428
499
  },
429
- lightboxModalSafeArea: {
430
- flex: 1,
500
+ gallery: {
431
501
  backgroundColor,
432
- justifyContent: 'center',
433
- alignItems: 'center',
502
+ },
503
+ galleryContentContainer: {
434
504
  paddingTop: top,
435
505
  paddingBottom: bottom,
436
506
  },
437
- lightboxImage: {
507
+ gestureImage: {
438
508
  height: '100%',
439
509
  width: WINDOW_WIDTH,
440
- backgroundColor,
510
+ backgroundColor: 'transparent',
441
511
  },
442
- lightboxImageLoading: {
512
+ gestureImageLoading: {
443
513
  backgroundColor,
444
514
  },
445
515
  actionToolbar: {
@@ -1 +1 @@
1
- {"version":3,"file":"image_attachment.js","sourceRoot":"","sources":["../../../../src/components/conversation/attachments/image_attachment.tsx"],"names":[],"mappings":"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","sourcesContent":["import React, { useMemo, useState, Dispatch, SetStateAction } from 'react'\nimport { StatusBar, StyleSheet, Modal, View, Linking, Dimensions, Platform } from 'react-native'\nimport { useSafeAreaInsets } from 'react-native-safe-area-context'\nimport {\n Gesture,\n GestureDetector,\n GestureHandlerRootView,\n Pressable,\n} from 'react-native-gesture-handler'\nimport Animated, {\n runOnJS,\n useAnimatedStyle,\n useAnimatedReaction,\n useSharedValue,\n withSpring,\n withDecay,\n} from 'react-native-reanimated'\nimport { tokens } from '../../../vendor/tapestry/tokens'\nimport { IconButton, Image, Heading, Text } from '../../display'\nimport colorFunction from 'color'\nimport { formatDatePreview } from '../../../utils/date'\nimport { DenormalizedMessageAttachmentResource } from '../../../types/resources/denormalized_attachment_resource'\nimport { PlatformPressable } from '@react-navigation/elements'\nimport { useTheme } from '../../../hooks'\n\nconst { width: WINDOW_WIDTH, height: WINDOW_HEIGHT } = Dimensions.get('window')\nconst DISMISS_PAN_THRESHOLD = 300\nconst MIN_DISTANCE_PAN_FOR_PLATFORM = Platform.OS === 'android' ? 5 : 0 // Android requires a higher threshold to give pinching priority\nconst POINTER_FOR_SINGLE_FINGER_PAN = 1 // Single-finger panning helps to avoid conflicts with pinching\nconst DEFAULT_OPACITY = 1\nconst DEFAULT_SCALE = 1\nconst MIN_SCALE = 0.5\nconst MAX_SCALE = 5\nconst MAX_SCALE_OVERSHOOT = 5\nconst DOUBLE_TAP_ZOOM_SCALE = 2\nconst DECAY_VELOCITY_FACTOR = 0.4\nconst RESET_SPRING_CONFIG = {\n damping: 20,\n stiffness: 150,\n}\n\nexport type MetaProps = {\n authorName: string\n createdAt: string\n}\n\nexport function ImageAttachment({\n attachment,\n metaProps,\n onMessageAttachmentLongPress,\n}: {\n attachment: DenormalizedMessageAttachmentResource\n metaProps: MetaProps\n onMessageAttachmentLongPress: (attachment: DenormalizedMessageAttachmentResource) => void\n}) {\n const { attributes } = attachment\n const { url, urlMedium, filename, metadata = {} } = attributes\n const { colors } = useTheme()\n\n const styles = useStyles({ imageWidth: metadata.width, imageHeight: metadata.height })\n const [visible, setVisible] = useState(false)\n\n return (\n <>\n <PlatformPressable\n style={styles.container}\n onPress={() => setVisible(true)}\n onLongPress={() => onMessageAttachmentLongPress(attachment)}\n android_ripple={{ color: colors.androidRippleNeutral, foreground: true }}\n accessibilityHint=\"Long press for more options\"\n >\n <Image\n source={{ uri: urlMedium || url }}\n style={styles.image}\n wrapperStyle={styles.imageWrapper}\n alt={filename}\n />\n </PlatformPressable>\n <LightboxModal\n visible={visible}\n setModalVisible={setVisible}\n uri={urlMedium || url}\n metaProps={metaProps}\n imageWidth={metadata.width}\n imageHeight={metadata.height}\n />\n </>\n )\n}\n\ninterface LightboxModalProps {\n visible: boolean\n setModalVisible: Dispatch<SetStateAction<boolean>>\n uri: string\n metaProps: MetaProps\n imageWidth?: number\n imageHeight?: number\n}\n\nconst LightboxModal = ({\n uri,\n visible,\n setModalVisible,\n metaProps,\n imageWidth,\n imageHeight,\n}: LightboxModalProps) => {\n const styles = useStyles()\n const insets = useSafeAreaInsets()\n\n const { authorName, createdAt } = metaProps\n\n // Calculate available space for image display\n const availableWindowWidth = WINDOW_WIDTH\n const availableWindowHeight = WINDOW_HEIGHT - insets.top - insets.bottom\n\n /* ============================\n ANIMATION VALUES\n ============================ */\n // Native State:\n const dismissY = useSharedValue(0) // vertical distance to dismiss modal\n const opacity = useSharedValue(DEFAULT_OPACITY) // opacity of modal\n const scale = useSharedValue(DEFAULT_SCALE) // zoom level of image\n const focalX = useSharedValue(0) // focal point of image between fingers\n const focalY = useSharedValue(0) // focal point of image between fingers\n const translateX = useSharedValue(0) // horizontal distance to pan image\n const translateY = useSharedValue(0) // vertical distance to pan image\n const savedScale = useSharedValue(DEFAULT_SCALE) // previous zoom level\n const savedTranslateX = useSharedValue(0) // previous horizontal position\n const savedTranslateY = useSharedValue(0) // previous vertical position\n const toolbarVisible = useSharedValue(1) // toolbar visibility state\n // React (JS) State:\n const [isStatusBarHidden, setIsStatusBarHidden] = useState(false)\n\n // Syncs toolbar useSharedValue state with React's state so that the status bar can be hidden/shown based on the toolbar's animation\n useAnimatedReaction(\n () => toolbarVisible.value,\n value => {\n runOnJS(setIsStatusBarHidden)(value === 0)\n }\n )\n\n /* ============================\n HANDLERS\n ============================ */\n const handleOpenInBrowser = () => {\n Linking.openURL(uri)\n }\n\n const resetDismissGestures = () => {\n dismissY.value = withSpring(0, RESET_SPRING_CONFIG)\n opacity.value = withSpring(DEFAULT_OPACITY, RESET_SPRING_CONFIG)\n }\n\n const resetAllGestures = () => {\n resetDismissGestures()\n scale.value = withSpring(DEFAULT_SCALE, RESET_SPRING_CONFIG)\n translateX.value = withSpring(0, RESET_SPRING_CONFIG)\n translateY.value = withSpring(0, RESET_SPRING_CONFIG)\n savedScale.value = DEFAULT_SCALE\n savedTranslateX.value = 0\n savedTranslateY.value = 0\n toolbarVisible.value = withSpring(1, RESET_SPRING_CONFIG)\n }\n\n const handleCloseModal = () => {\n setModalVisible(false)\n resetAllGestures()\n }\n\n /* ============================\n UTILITY FUNCTIONS\n 'worklet' runs functions on the UI thread, instead of the JS thread for better performance\n ============================ */\n const getImageContainedToWindowDimensions = () => {\n 'worklet'\n\n if (!imageWidth || !imageHeight) {\n return { width: availableWindowWidth, height: availableWindowHeight }\n }\n\n const imageAspectRatio = imageWidth / imageHeight\n const windowAspectRatio = availableWindowWidth / availableWindowHeight\n\n // Constrain image width if its wider than window\n if (imageAspectRatio > windowAspectRatio) {\n return {\n width: availableWindowWidth,\n height: availableWindowWidth / imageAspectRatio,\n }\n }\n\n // Constrain image height if its taller than window\n return {\n width: availableWindowHeight * imageAspectRatio,\n height: availableWindowHeight,\n }\n }\n\n const zoomToFocalPoint = ({\n focalPointX,\n focalPointY,\n targetScale,\n currentSavedScale,\n currentSavedTranslateX,\n currentSavedTranslateY,\n }: {\n focalPointX: number\n focalPointY: number\n targetScale: number\n currentSavedScale: number\n currentSavedTranslateX: number\n currentSavedTranslateY: number\n }) => {\n 'worklet'\n\n // How far the focal point is from the center of the window\n const windowCenterX = WINDOW_WIDTH / 2\n const windowCenterY = WINDOW_HEIGHT / 2\n\n // Position of focal point relative to current image position\n const focalRelativeX = focalPointX - windowCenterX - currentSavedTranslateX\n const focalRelativeY = focalPointY - windowCenterY - currentSavedTranslateY\n\n // Calculate new translation to keep focal point under fingers\n const scaleRatio = targetScale / currentSavedScale\n const newTranslateX = currentSavedTranslateX + focalRelativeX * (1 - scaleRatio)\n const newTranslateY = currentSavedTranslateY + focalRelativeY * (1 - scaleRatio)\n\n return {\n translateX: newTranslateX,\n translateY: newTranslateY,\n }\n }\n\n const getMaxTranslationLimits = (currentScale: number) => {\n 'worklet'\n\n const { width: containedImageWidth, height: containedImageHeight } =\n getImageContainedToWindowDimensions()\n\n // Image dimensions after scaling/zooming\n const scaledWidth = containedImageWidth * currentScale\n const scaledHeight = containedImageHeight * currentScale\n\n // How much the scaled image exceeds the window container\n const excessWidth = scaledWidth - availableWindowWidth\n const excessHeight = scaledHeight - availableWindowHeight\n\n // How far the image can move in each direction before hitting window edges\n const maxTranslateX = Math.max(0, excessWidth / 2)\n const maxTranslateY = Math.max(0, excessHeight / 2)\n\n return { maxTranslateX, maxTranslateY }\n }\n\n const clampDecay = (currentScale: number): { x: [number, number]; y: [number, number] } => {\n 'worklet'\n\n const { maxTranslateX, maxTranslateY } = getMaxTranslationLimits(currentScale)\n\n return {\n x: [-maxTranslateX, maxTranslateX],\n y: [-maxTranslateY, maxTranslateY],\n }\n }\n\n const clampTranslation = ({\n x,\n y,\n currentScale,\n }: {\n x: number\n y: number\n currentScale: number\n }) => {\n 'worklet'\n\n const { maxTranslateX, maxTranslateY } = getMaxTranslationLimits(currentScale)\n\n return {\n x: Math.min(maxTranslateX, Math.max(-maxTranslateX, x)),\n y: Math.min(maxTranslateY, Math.max(-maxTranslateY, y)),\n }\n }\n\n const isImageAtVerticalBoundry = (currentScale: number) => {\n 'worklet'\n\n const { height: containedImageHeight } = getImageContainedToWindowDimensions()\n\n // Calculate how much the image can exceed the window container\n const scaledHeight = containedImageHeight * currentScale\n const excessHeight = scaledHeight - availableWindowHeight\n const maxTranslateY = Math.max(0, excessHeight / 2)\n\n const currentTranslateY = translateY.value\n const panPositionTolerance = 1 // buffer to account for translateY being at a subpixel position\n\n const atTopBoundry = currentTranslateY >= maxTranslateY - panPositionTolerance\n const atBottomBoundry = currentTranslateY <= -maxTranslateY + panPositionTolerance\n\n return atTopBoundry || atBottomBoundry\n }\n\n /* ============================\n GESTURES\n ============================ */\n const singleTapGesture = Gesture.Tap()\n .numberOfTaps(1)\n .onStart(() => {\n toolbarVisible.value = withSpring(toolbarVisible.value > 0.5 ? 0 : 1, RESET_SPRING_CONFIG)\n })\n\n const doubleTapGesture = Gesture.Tap()\n .numberOfTaps(2)\n .onStart(e => {\n const isZoomedIn = scale.value > DEFAULT_SCALE\n if (isZoomedIn) {\n runOnJS(resetAllGestures)()\n } else {\n // Hide toolbar when starting to zoom\n toolbarVisible.value = withSpring(0, RESET_SPRING_CONFIG)\n\n // Zoom to 2x at tap location\n const newTranslation = zoomToFocalPoint({\n focalPointX: e.x,\n focalPointY: e.y,\n targetScale: DOUBLE_TAP_ZOOM_SCALE,\n currentSavedScale: savedScale.value,\n currentSavedTranslateX: savedTranslateX.value,\n currentSavedTranslateY: savedTranslateY.value,\n })\n\n // Apply clamping to ensure image edges remain within window boundaries\n const clampedTranslate = clampTranslation({\n x: newTranslation.translateX,\n y: newTranslation.translateY,\n currentScale: DOUBLE_TAP_ZOOM_SCALE,\n })\n\n // Animate to new scale and translation\n scale.value = withSpring(DOUBLE_TAP_ZOOM_SCALE, RESET_SPRING_CONFIG)\n translateX.value = withSpring(clampedTranslate.x, RESET_SPRING_CONFIG)\n translateY.value = withSpring(clampedTranslate.y, RESET_SPRING_CONFIG)\n\n // Update saved state for next gesture\n savedScale.value = DOUBLE_TAP_ZOOM_SCALE\n savedTranslateX.value = clampedTranslate.x\n savedTranslateY.value = clampedTranslate.y\n }\n })\n\n const pinchGesture = Gesture.Pinch()\n .onStart(e => {\n focalX.value = e.focalX\n focalY.value = e.focalY\n\n // Hide toolbar when starting to zoom\n toolbarVisible.value = withSpring(0, RESET_SPRING_CONFIG)\n\n // Ensure that pinch accounts for the decay animation and starts from the true current position.\n savedTranslateX.value = translateX.value\n savedTranslateY.value = translateY.value\n })\n .onUpdate(e => {\n // Zoom image in/out within scale limits\n const newScale = savedScale.value * e.scale\n const newScaleClamped = Math.min(\n MAX_SCALE + MAX_SCALE_OVERSHOOT,\n Math.max(MIN_SCALE, newScale)\n )\n scale.value = newScaleClamped\n\n // Calculate new translation to keep focal point under fingers\n const newTranslation = zoomToFocalPoint({\n focalPointX: focalX.value,\n focalPointY: focalY.value,\n targetScale: newScaleClamped,\n currentSavedScale: savedScale.value,\n currentSavedTranslateX: savedTranslateX.value,\n currentSavedTranslateY: savedTranslateY.value,\n })\n\n // Apply translation without clamping to ensure focal point doesn't jump during gesture\n translateX.value = newTranslation.translateX\n translateY.value = newTranslation.translateY\n })\n .onEnd(() => {\n const currentScale = scale.value\n\n // Dismiss modal if fully zoomed out\n if (currentScale <= MIN_SCALE) {\n runOnJS(handleCloseModal)()\n return\n }\n\n // Reset all gestures if image is near default scale\n const isNearDefaultScale = currentScale <= DEFAULT_SCALE + 0.1\n if (isNearDefaultScale) {\n runOnJS(resetAllGestures)()\n return\n }\n\n // Check if overshooting the max scale and clamp it\n const finalScale = currentScale > MAX_SCALE ? MAX_SCALE : currentScale\n\n // Recalculate translation using focal point to always return to the same position when image is overshooting the max scale\n const newTranslation = zoomToFocalPoint({\n focalPointX: focalX.value,\n focalPointY: focalY.value,\n targetScale: finalScale,\n currentSavedScale: savedScale.value,\n currentSavedTranslateX: savedTranslateX.value,\n currentSavedTranslateY: savedTranslateY.value,\n })\n\n // Ensure image edges remain within window boundaries\n const clampedTranslate = clampTranslation({\n x: newTranslation.translateX,\n y: newTranslation.translateY,\n currentScale: finalScale,\n })\n\n // Animate to position and scale limits with a spring\n scale.value = withSpring(finalScale, RESET_SPRING_CONFIG)\n translateX.value = withSpring(clampedTranslate.x, RESET_SPRING_CONFIG)\n translateY.value = withSpring(clampedTranslate.y, RESET_SPRING_CONFIG)\n\n // Save state for next gesture\n savedScale.value = finalScale\n savedTranslateX.value = clampedTranslate.x\n savedTranslateY.value = clampedTranslate.y\n })\n\n const panGesture = Gesture.Pan()\n .minDistance(MIN_DISTANCE_PAN_FOR_PLATFORM)\n .minPointers(POINTER_FOR_SINGLE_FINGER_PAN)\n .maxPointers(POINTER_FOR_SINGLE_FINGER_PAN)\n .onStart(() => {\n // Update saved position to current animated position\n // This ensures smooth continuation if decay is running\n savedTranslateX.value = translateX.value\n savedTranslateY.value = translateY.value\n })\n .onUpdate(e => {\n // Only pan if image is zoomed in\n if (scale.value <= DEFAULT_SCALE) return\n\n const newTranslateX = savedTranslateX.value + e.translationX\n const newTranslateY = savedTranslateY.value + e.translationY\n\n // Ensure image edges remain within window boundaries\n const clampedTranslate = clampTranslation({\n x: newTranslateX,\n y: newTranslateY,\n currentScale: scale.value,\n })\n translateX.value = clampedTranslate.x\n translateY.value = clampedTranslate.y\n })\n .onEnd(e => {\n // Prevents saving pan position if image is zoomed out while panning\n if (scale.value <= DEFAULT_SCALE) return\n\n const clampDecayBounds = clampDecay(scale.value)\n\n translateX.value = withDecay(\n {\n velocity: e.velocityX * DECAY_VELOCITY_FACTOR,\n clamp: clampDecayBounds.x,\n deceleration: 0.996,\n },\n finished => {\n if (finished) {\n savedTranslateX.value = translateX.value\n }\n }\n )\n\n translateY.value = withDecay(\n {\n velocity: e.velocityY * DECAY_VELOCITY_FACTOR,\n clamp: clampDecayBounds.y,\n deceleration: 0.996,\n },\n finished => {\n if (finished) {\n savedTranslateY.value = translateY.value\n }\n }\n )\n })\n\n const panToDismissModalGesture = Gesture.Pan()\n .onUpdate(e => {\n const atDefaultScale = scale.value === DEFAULT_SCALE\n\n // Calculate zoom conditions for dismissing modal\n const atVerticalBoundry = isImageAtVerticalBoundry(scale.value)\n const panDirectionIsPrimarilyVertical = Math.abs(e.translationY) > Math.abs(e.translationX)\n const canDismissWhileZoomed = atVerticalBoundry && panDirectionIsPrimarilyVertical\n\n if (atDefaultScale || canDismissWhileZoomed) {\n const panDistance = Math.abs(e.translationY)\n const fadeProgress = panDistance / DISMISS_PAN_THRESHOLD\n\n opacity.value = Math.max(0, DEFAULT_OPACITY - fadeProgress)\n dismissY.value = e.translationY\n }\n })\n .onEnd(() => {\n const exceededDismissThreshold = Math.abs(dismissY.value) > DISMISS_PAN_THRESHOLD\n\n if (exceededDismissThreshold) {\n runOnJS(handleCloseModal)()\n } else {\n runOnJS(resetDismissGestures)()\n }\n })\n\n /* ==============================\n IMPLEMENT GESTURES & ANIMATIONS\n ================================= */\n // Race between pinch and pan ensures only one is active at a time, preserving focal point logic\n const pinchOrPanGestures = Gesture.Race(pinchGesture, panGesture)\n\n // Exclusive race ensures single tap doesn't interfere with double tap\n const tapGestures = Gesture.Exclusive(doubleTapGesture, singleTapGesture)\n\n // Race between tap gestures and pinch/pan\n const transformImageGestures = Gesture.Race(tapGestures, pinchOrPanGestures)\n\n // Dismiss can work simultaneously with all gestures\n const composedGesture = Gesture.Simultaneous(transformImageGestures, panToDismissModalGesture)\n\n const animatedImageStyles = useAnimatedStyle(() => ({\n transform: [\n { translateX: translateX.value },\n { translateY: translateY.value + dismissY.value },\n { scale: scale.value },\n ],\n opacity: opacity.value,\n }))\n\n const animatedToolbarStyles = useAnimatedStyle(() => ({\n opacity: toolbarVisible.value,\n transform: [\n { translateY: (1 - toolbarVisible.value) * -20 }, // slide up when hiding\n ],\n }))\n\n return (\n <Modal visible={visible} transparent animationType=\"fade\" onRequestClose={handleCloseModal}>\n <StatusBar\n barStyle=\"light-content\"\n hidden={isStatusBarHidden}\n animated\n showHideTransition=\"slide\"\n />\n <View style={styles.lightboxModalSafeArea}>\n <GestureHandlerRootView>\n <GestureDetector gesture={composedGesture}>\n <PreventPressEventsBubbling>\n <Image\n source={{ uri }}\n loadingBackgroundStyles={styles.lightboxImageLoading}\n style={styles.lightboxImage}\n animatedImageStyle={animatedImageStyles}\n resizeMode=\"contain\"\n animated={true}\n alt=\"\"\n />\n </PreventPressEventsBubbling>\n </GestureDetector>\n </GestureHandlerRootView>\n </View>\n <Animated.View\n style={[styles.actionToolbar, animatedToolbarStyles]}\n accessibilityRole=\"toolbar\"\n >\n <View style={styles.actionToolbarTextMeta}>\n <Heading variant=\"h3\" style={styles.actionToolbarTitle} numberOfLines={1}>\n {authorName}\n </Heading>\n <Text variant=\"tertiary\" style={styles.actionToolbarSubtitle}>\n {formatDatePreview(createdAt)}\n </Text>\n </View>\n <IconButton\n onPress={handleOpenInBrowser}\n name=\"general.newWindow\"\n accessibilityRole=\"link\"\n accessibilityLabel=\"Open image in browser\"\n accessibilityHint=\"Image can be downloaded and shared through the browser.\"\n style={styles.actionButton}\n iconStyle={styles.actionButtonIcon}\n size=\"lg\"\n />\n <IconButton\n onPress={handleCloseModal}\n name=\"general.x\"\n accessibilityLabel=\"Close image\"\n style={styles.actionButton}\n iconStyle={styles.actionButtonIcon}\n size=\"lg\"\n />\n </Animated.View>\n </Modal>\n )\n}\n\nconst PreventPressEventsBubbling = ({ children }: { children: React.ReactNode }) => {\n return (\n <Pressable onLongPress={() => {}} onPress={() => {}}>\n {children}\n </Pressable>\n )\n}\n\ninterface UseStylesProps {\n imageWidth?: number\n imageHeight?: number\n}\n\nconst useStyles = ({ imageWidth = 100, imageHeight = 100 }: UseStylesProps = {}) => {\n const { top, bottom } = useSafeAreaInsets()\n const backgroundColor = tokens.colorNeutral7\n const transparentBackgroundColor = useMemo(\n () => colorFunction(backgroundColor).alpha(0.8).toString(),\n [backgroundColor]\n )\n\n return StyleSheet.create({\n container: {\n maxWidth: '100%',\n },\n imageWrapper: {\n width: '100%',\n minWidth: 200,\n aspectRatio: imageWidth / imageHeight,\n },\n image: {\n borderRadius: 8,\n },\n lightboxModalSafeArea: {\n flex: 1,\n backgroundColor,\n justifyContent: 'center',\n alignItems: 'center',\n paddingTop: top,\n paddingBottom: bottom,\n },\n lightboxImage: {\n height: '100%',\n width: WINDOW_WIDTH,\n backgroundColor,\n },\n lightboxImageLoading: {\n backgroundColor,\n },\n actionToolbar: {\n width: '100%',\n position: 'absolute',\n top: 0,\n flexDirection: 'row',\n alignItems: 'center',\n gap: 20,\n paddingHorizontal: 16,\n paddingTop: top + 16,\n paddingBottom: 8,\n backgroundColor: transparentBackgroundColor,\n },\n actionToolbarTextMeta: {\n flex: 1,\n },\n actionToolbarTitle: {\n marginRight: 'auto',\n flexShrink: 1,\n color: tokens.colorNeutral88,\n },\n actionToolbarSubtitle: {\n color: tokens.colorNeutral68,\n },\n actionButton: {\n backgroundColor,\n height: 40,\n width: 40,\n borderRadius: 50,\n borderWidth: 1,\n borderColor: tokens.colorNeutral24,\n },\n actionButtonIcon: {\n color: tokens.colorNeutral88,\n },\n })\n}\n"]}
1
+ {"version":3,"file":"image_attachment.js","sourceRoot":"","sources":["../../../../src/components/conversation/attachments/image_attachment.tsx"],"names":[],"mappings":"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","sourcesContent":["import React, { useMemo, useState, Dispatch, SetStateAction, useCallback } from 'react'\nimport {\n StatusBar,\n StyleSheet,\n Modal,\n View,\n Linking,\n Dimensions,\n Platform,\n NativeSyntheticEvent,\n NativeScrollEvent,\n ViewToken,\n} from 'react-native'\nimport { useSafeAreaInsets } from 'react-native-safe-area-context'\nimport {\n FlatList,\n Gesture,\n GestureDetector,\n GestureHandlerRootView,\n Pressable,\n} from 'react-native-gesture-handler'\nimport Animated, {\n runOnJS,\n useAnimatedStyle,\n useAnimatedReaction,\n useSharedValue,\n withSpring,\n withDecay,\n SharedValue,\n} from 'react-native-reanimated'\nimport { tokens } from '../../../vendor/tapestry/tokens'\nimport { IconButton, Image, Heading, Text } from '../../display'\nimport colorFunction from 'color'\nimport { formatDatePreview } from '../../../utils/date'\nimport { DenormalizedMessageAttachmentResource } from '../../../types/resources/denormalized_attachment_resource'\nimport { PlatformPressable } from '@react-navigation/elements'\nimport { useTheme } from '../../../hooks'\n\nconst { width: WINDOW_WIDTH, height: WINDOW_HEIGHT } = Dimensions.get('window')\nconst DISMISS_PAN_THRESHOLD = 300\nconst MIN_DISTANCE_PAN_FOR_PLATFORM = Platform.OS === 'android' ? 5 : 0 // Android requires a higher threshold to give pinching priority\nconst POINTER_FOR_SINGLE_FINGER_PAN = 1 // Single-finger panning helps to avoid conflicts with pinching\nconst DEFAULT_OPACITY = 1\nconst DEFAULT_TRANSLATE_X = 0\nconst DEFAULT_TRANSLATE_Y = 0\nconst DEFAULT_SCALE = 1\nconst MIN_SCALE = 0.5\nconst MAX_SCALE = 5\nconst MAX_SCALE_OVERSHOOT = 5\nconst DOUBLE_TAP_ZOOM_SCALE = 2\nconst DECAY_VELOCITY_FACTOR = 0.4\nconst RESET_SPRING_CONFIG = {\n damping: 20,\n stiffness: 150,\n}\n\nexport type MetaProps = {\n authorName: string\n createdAt: string\n}\n\nexport function ImageAttachment({\n attachment,\n imageAttachments,\n currentImageIndex,\n metaProps,\n onMessageAttachmentLongPress,\n}: {\n attachment: DenormalizedMessageAttachmentResource\n imageAttachments: DenormalizedMessageAttachmentResource[]\n currentImageIndex: number\n metaProps: MetaProps\n onMessageAttachmentLongPress: (attachment: DenormalizedMessageAttachmentResource) => void\n}) {\n const { attributes } = attachment\n const { url, urlMedium, filename, metadata = {} } = attributes\n const { colors } = useTheme()\n\n const styles = useStyles({ imageWidth: metadata.width, imageHeight: metadata.height })\n const [visible, setVisible] = useState(false)\n\n return (\n <>\n <PlatformPressable\n style={styles.container}\n onPress={() => setVisible(true)}\n onLongPress={() => onMessageAttachmentLongPress(attachment)}\n android_ripple={{ color: colors.androidRippleNeutral, foreground: true }}\n accessibilityHint=\"Long press for more options\"\n >\n <Image\n source={{ uri: urlMedium || url }}\n style={styles.image}\n wrapperStyle={styles.attachmentImageWrapper}\n alt={filename}\n />\n </PlatformPressable>\n <LightboxModal\n visible={visible}\n setModalVisible={setVisible}\n imageAttachments={imageAttachments}\n initialImageIndex={currentImageIndex}\n metaProps={metaProps}\n />\n </>\n )\n}\n\ninterface LightboxModalProps {\n visible: boolean\n setModalVisible: Dispatch<SetStateAction<boolean>>\n imageAttachments: DenormalizedMessageAttachmentResource[]\n initialImageIndex: number\n metaProps: MetaProps\n}\n\nconst LightboxModal = ({\n visible,\n setModalVisible,\n imageAttachments,\n initialImageIndex,\n metaProps,\n}: LightboxModalProps) => {\n const styles = useStyles()\n const insets = useSafeAreaInsets()\n const [currentImageIndex, setCurrentImageIndex] = useState(initialImageIndex)\n\n const { authorName, createdAt } = metaProps\n\n // Get current image data\n const currentImage = imageAttachments[currentImageIndex]\n const { url, urlMedium, metadata = {} } = currentImage.attributes\n const imageWidth = metadata.width\n const imageHeight = metadata.height\n\n // Calculate available space for image display\n const availableWindowWidth = WINDOW_WIDTH\n const availableWindowHeight = WINDOW_HEIGHT - insets.top - insets.bottom\n\n /* ============================\n ANIMATION VALUES\n ============================ */\n // Native State:\n const dismissY = useSharedValue(0) // vertical distance to dismiss modal\n const opacity = useSharedValue(DEFAULT_OPACITY) // opacity of modal\n const scale = useSharedValue(DEFAULT_SCALE) // zoom level of image\n const focalX = useSharedValue(0) // focal point of image between fingers\n const focalY = useSharedValue(0) // focal point of image between fingers\n const translateX = useSharedValue(DEFAULT_TRANSLATE_X) // horizontal distance to pan image\n const translateY = useSharedValue(DEFAULT_TRANSLATE_Y) // vertical distance to pan image\n const savedScale = useSharedValue(DEFAULT_SCALE) // previous zoom level\n const savedTranslateX = useSharedValue(DEFAULT_TRANSLATE_X) // previous horizontal position\n const savedTranslateY = useSharedValue(DEFAULT_TRANSLATE_Y) // previous vertical position\n const toolbarVisible = useSharedValue(1) // toolbar visibility state\n\n // React (JS) State:\n const [isStatusBarHidden, setIsStatusBarHidden] = useState(false)\n const [flatListScrollEnabled, setFlatListScrollEnabled] = useState(true)\n const [panGestureEnabled, setPanGestureEnabled] = useState(false)\n\n // Syncs toolbar useSharedValue state with React's state so that the status bar can be hidden/shown based on the toolbar's animation\n useAnimatedReaction(\n () => toolbarVisible.value,\n value => {\n runOnJS(setIsStatusBarHidden)(value === 0)\n }\n )\n\n // Syncs FlatList scroll state with scale changes\n // When image is at default scale, enable scroll and disable pan gesture\n // When image is zoomed in, disable scroll and enable pan gesture\n useAnimatedReaction(\n () => scale.value,\n value => {\n const enableFlatListScroll = value === DEFAULT_SCALE\n runOnJS(setFlatListScrollEnabled)(enableFlatListScroll)\n runOnJS(setPanGestureEnabled)(!enableFlatListScroll)\n }\n )\n\n /* ============================\n HANDLERS\n ============================ */\n const handleOpenInBrowser = useCallback(() => {\n Linking.openURL(urlMedium || url)\n }, [urlMedium, url])\n\n const resetDismissGestures = useCallback(() => {\n dismissY.value = withSpring(0, RESET_SPRING_CONFIG)\n opacity.value = withSpring(DEFAULT_OPACITY, RESET_SPRING_CONFIG)\n }, [dismissY, opacity])\n\n const resetAllGestures = useCallback(() => {\n resetDismissGestures()\n scale.value = withSpring(DEFAULT_SCALE, RESET_SPRING_CONFIG)\n translateX.value = withSpring(DEFAULT_TRANSLATE_X, RESET_SPRING_CONFIG)\n translateY.value = withSpring(DEFAULT_TRANSLATE_Y, RESET_SPRING_CONFIG)\n savedScale.value = DEFAULT_SCALE\n savedTranslateX.value = DEFAULT_TRANSLATE_X\n savedTranslateY.value = DEFAULT_TRANSLATE_Y\n toolbarVisible.value = withSpring(1, RESET_SPRING_CONFIG)\n }, [\n resetDismissGestures,\n scale,\n translateX,\n translateY,\n savedScale,\n savedTranslateX,\n savedTranslateY,\n toolbarVisible,\n ])\n\n const handleCloseModal = useCallback(() => {\n setModalVisible(false)\n resetAllGestures()\n }, [setModalVisible, resetAllGestures])\n\n /* ============================\n UTILITY WORKLET FUNCTIONS\n 'worklet' runs functions on the UI thread, instead of the JS thread for better performance.\n ============================ */\n const getImageContainedToWindowDimensions = () => {\n 'worklet'\n\n if (!imageWidth || !imageHeight) {\n return { width: availableWindowWidth, height: availableWindowHeight }\n }\n\n const imageAspectRatio = imageWidth / imageHeight\n const windowAspectRatio = availableWindowWidth / availableWindowHeight\n\n // Constrain image width if its wider than window\n if (imageAspectRatio > windowAspectRatio) {\n return {\n width: availableWindowWidth,\n height: availableWindowWidth / imageAspectRatio,\n }\n }\n\n // Constrain image height if its taller than window\n return {\n width: availableWindowHeight * imageAspectRatio,\n height: availableWindowHeight,\n }\n }\n\n const zoomToFocalPoint = ({\n focalPointX,\n focalPointY,\n targetScale,\n currentSavedScale,\n currentSavedTranslateX,\n currentSavedTranslateY,\n }: {\n focalPointX: number\n focalPointY: number\n targetScale: number\n currentSavedScale: number\n currentSavedTranslateX: number\n currentSavedTranslateY: number\n }) => {\n 'worklet'\n\n // How far the focal point is from the center of the window\n const windowCenterX = WINDOW_WIDTH / 2\n const windowCenterY = WINDOW_HEIGHT / 2\n\n // Position of focal point relative to current image position\n const focalRelativeX = focalPointX - windowCenterX - currentSavedTranslateX\n const focalRelativeY = focalPointY - windowCenterY - currentSavedTranslateY\n\n // Calculate new translation to keep focal point under fingers\n const scaleRatio = targetScale / currentSavedScale\n const newTranslateX = currentSavedTranslateX + focalRelativeX * (1 - scaleRatio)\n const newTranslateY = currentSavedTranslateY + focalRelativeY * (1 - scaleRatio)\n\n return {\n translateX: newTranslateX,\n translateY: newTranslateY,\n }\n }\n\n const getMaxTranslationLimits = (currentScale: number) => {\n 'worklet'\n\n const { width: containedImageWidth, height: containedImageHeight } =\n getImageContainedToWindowDimensions()\n\n // Image dimensions after scaling/zooming\n const scaledWidth = containedImageWidth * currentScale\n const scaledHeight = containedImageHeight * currentScale\n\n // How much the scaled image exceeds the window container\n const excessWidth = scaledWidth - availableWindowWidth\n const excessHeight = scaledHeight - availableWindowHeight\n\n // How far the image can move in each direction before hitting window edges\n const maxTranslateX = Math.max(DEFAULT_TRANSLATE_X, excessWidth / 2)\n const maxTranslateY = Math.max(DEFAULT_TRANSLATE_Y, excessHeight / 2)\n\n return { maxTranslateX, maxTranslateY }\n }\n\n const clampDecay = (currentScale: number): { x: [number, number]; y: [number, number] } => {\n 'worklet'\n\n const { maxTranslateX, maxTranslateY } = getMaxTranslationLimits(currentScale)\n\n return {\n x: [-maxTranslateX, maxTranslateX],\n y: [-maxTranslateY, maxTranslateY],\n }\n }\n\n const clampTranslation = ({\n x,\n y,\n currentScale,\n }: {\n x: number\n y: number\n currentScale: number\n }) => {\n 'worklet'\n\n const { maxTranslateX, maxTranslateY } = getMaxTranslationLimits(currentScale)\n\n return {\n x: Math.min(maxTranslateX, Math.max(-maxTranslateX, x)),\n y: Math.min(maxTranslateY, Math.max(-maxTranslateY, y)),\n }\n }\n\n const isImageAtVerticalBoundry = (currentScale: number) => {\n 'worklet'\n\n const { height: containedImageHeight } = getImageContainedToWindowDimensions()\n\n // Calculate how much the image can exceed the window container\n const scaledHeight = containedImageHeight * currentScale\n const excessHeight = scaledHeight - availableWindowHeight\n const maxTranslateY = Math.max(DEFAULT_TRANSLATE_Y, excessHeight / 2)\n\n const currentTranslateY = translateY.value\n const panPositionTolerance = 1 // buffer to account for translateY being at a subpixel position\n\n const atTopBoundry = currentTranslateY >= maxTranslateY - panPositionTolerance\n const atBottomBoundry = currentTranslateY <= -maxTranslateY + panPositionTolerance\n\n return atTopBoundry || atBottomBoundry\n }\n\n /* ============================\n UTILITY FLATLIST FUNCTIONS\n Supports the image gallery layout and swipe functionality.\n ============================ */\n\n // Used in tandem with FlatList's initialScrollIndex to quickly calculate the position and size of each image before they load.\n const getItemLayout = useCallback(\n (_: unknown, index: number) => ({\n length: WINDOW_WIDTH,\n offset: WINDOW_WIDTH * index,\n index,\n }),\n []\n )\n\n // Captures the current image's index after the FlatList finishes its scroll animation.\n // Used in tandem with onViewableItemsChanged to ensure the final value for currentImageIndex is set.\n const onMomentumScrollEnd = useCallback(\n (event: NativeSyntheticEvent<NativeScrollEvent>) => {\n // Calculate the index of the image that is currently visible\n const imageOffsetX = event.nativeEvent.contentOffset.x\n const newImageIndex = Math.round(imageOffsetX / WINDOW_WIDTH)\n\n // Check if the image index has changed and the FlatList didn't scroll past the first or last image\n const didImageIndexChange = newImageIndex !== currentImageIndex\n const isImageIndexWithinBounds = newImageIndex >= 0 && newImageIndex < imageAttachments.length\n\n if (didImageIndexChange && isImageIndexWithinBounds) {\n setCurrentImageIndex(newImageIndex)\n }\n },\n [currentImageIndex, imageAttachments.length]\n )\n\n // Supplements onMomentumScrollEnd by capturing the current image's index while the FlatList is actively scrolling.\n // Used in tandem with viewabilityConfig to trigger when the image is 50% visible in the window.\n const onViewableItemsChanged = useCallback(\n ({ viewableItems }: { viewableItems: ViewToken<DenormalizedMessageAttachmentResource>[] }) => {\n if (viewableItems.length === 0) return\n\n // Use the first viewable item which is enforced by the FlatList's pagingEnabled prop that allows only two images to be visible at a time when scrolling.\n const firstViewableItem = viewableItems[0]\n const newIndex = firstViewableItem.index\n\n if (newIndex !== null && newIndex !== currentImageIndex) {\n setCurrentImageIndex(newIndex)\n }\n },\n [currentImageIndex]\n )\n\n /* ============================\n GESTURES\n ============================ */\n const singleTapGesture = Gesture.Tap()\n .numberOfTaps(1)\n .onStart(() => {\n toolbarVisible.value = withSpring(toolbarVisible.value > 0.5 ? 0 : 1, RESET_SPRING_CONFIG)\n })\n\n const doubleTapGesture = Gesture.Tap()\n .numberOfTaps(2)\n .onStart(e => {\n const isZoomedIn = scale.value > DEFAULT_SCALE\n if (isZoomedIn) {\n runOnJS(resetAllGestures)()\n } else {\n // Hide toolbar when starting to zoom\n toolbarVisible.value = withSpring(0, RESET_SPRING_CONFIG)\n\n // Zoom to 2x at tap location\n const newTranslation = zoomToFocalPoint({\n focalPointX: e.x,\n focalPointY: e.y,\n targetScale: DOUBLE_TAP_ZOOM_SCALE,\n currentSavedScale: savedScale.value,\n currentSavedTranslateX: savedTranslateX.value,\n currentSavedTranslateY: savedTranslateY.value,\n })\n\n // Apply clamping to ensure image edges remain within window boundaries\n const clampedTranslate = clampTranslation({\n x: newTranslation.translateX,\n y: newTranslation.translateY,\n currentScale: DOUBLE_TAP_ZOOM_SCALE,\n })\n\n // Animate to new scale and translation\n scale.value = withSpring(DOUBLE_TAP_ZOOM_SCALE, RESET_SPRING_CONFIG)\n translateX.value = withSpring(clampedTranslate.x, RESET_SPRING_CONFIG)\n translateY.value = withSpring(clampedTranslate.y, RESET_SPRING_CONFIG)\n\n // Update saved state for next gesture\n savedScale.value = DOUBLE_TAP_ZOOM_SCALE\n savedTranslateX.value = clampedTranslate.x\n savedTranslateY.value = clampedTranslate.y\n }\n })\n\n const pinchGesture = Gesture.Pinch()\n .onStart(e => {\n focalX.value = e.focalX\n focalY.value = e.focalY\n\n // Hide toolbar when starting to zoom\n toolbarVisible.value = withSpring(0, RESET_SPRING_CONFIG)\n\n // Ensure that pinch accounts for the decay animation and starts from the true current position.\n savedTranslateX.value = translateX.value\n savedTranslateY.value = translateY.value\n })\n .onUpdate(e => {\n // Zoom image in/out within scale limits\n const newScale = savedScale.value * e.scale\n const newScaleClamped = Math.min(\n MAX_SCALE + MAX_SCALE_OVERSHOOT,\n Math.max(MIN_SCALE, newScale)\n )\n scale.value = newScaleClamped\n\n // Calculate new translation to keep focal point under fingers\n const newTranslation = zoomToFocalPoint({\n focalPointX: focalX.value,\n focalPointY: focalY.value,\n targetScale: newScaleClamped,\n currentSavedScale: savedScale.value,\n currentSavedTranslateX: savedTranslateX.value,\n currentSavedTranslateY: savedTranslateY.value,\n })\n\n // Apply translation without clamping to ensure focal point doesn't jump during gesture\n translateX.value = newTranslation.translateX\n translateY.value = newTranslation.translateY\n })\n .onEnd(() => {\n const currentScale = scale.value\n\n // Dismiss modal if fully zoomed out\n if (currentScale <= MIN_SCALE) {\n runOnJS(handleCloseModal)()\n return\n }\n\n // Reset all gestures if image is near default scale\n const isNearDefaultScale = currentScale <= DEFAULT_SCALE + 0.1\n if (isNearDefaultScale) {\n runOnJS(resetAllGestures)()\n return\n }\n\n // Check if overshooting the max scale and clamp it\n const finalScale = currentScale > MAX_SCALE ? MAX_SCALE : currentScale\n\n // Recalculate translation using focal point to always return to the same position when image is overshooting the max scale\n const newTranslation = zoomToFocalPoint({\n focalPointX: focalX.value,\n focalPointY: focalY.value,\n targetScale: finalScale,\n currentSavedScale: savedScale.value,\n currentSavedTranslateX: savedTranslateX.value,\n currentSavedTranslateY: savedTranslateY.value,\n })\n\n // Ensure image edges remain within window boundaries\n const clampedTranslate = clampTranslation({\n x: newTranslation.translateX,\n y: newTranslation.translateY,\n currentScale: finalScale,\n })\n\n // Animate to position and scale limits with a spring\n scale.value = withSpring(finalScale, RESET_SPRING_CONFIG)\n translateX.value = withSpring(clampedTranslate.x, RESET_SPRING_CONFIG)\n translateY.value = withSpring(clampedTranslate.y, RESET_SPRING_CONFIG)\n\n // Save state for next gesture\n savedScale.value = finalScale\n savedTranslateX.value = clampedTranslate.x\n savedTranslateY.value = clampedTranslate.y\n })\n\n const panGesture = Gesture.Pan()\n .minDistance(MIN_DISTANCE_PAN_FOR_PLATFORM)\n .minPointers(POINTER_FOR_SINGLE_FINGER_PAN)\n .maxPointers(POINTER_FOR_SINGLE_FINGER_PAN)\n .enabled(panGestureEnabled)\n .onStart(() => {\n // Update saved position to current animated position\n // This ensures smooth continuation if decay is running\n savedTranslateX.value = translateX.value\n savedTranslateY.value = translateY.value\n })\n .onUpdate(e => {\n // Only pan if image is zoomed in\n if (scale.value <= DEFAULT_SCALE) return\n\n const newTranslateX = savedTranslateX.value + e.translationX\n const newTranslateY = savedTranslateY.value + e.translationY\n\n // Ensure image edges remain within window boundaries\n const clampedTranslate = clampTranslation({\n x: newTranslateX,\n y: newTranslateY,\n currentScale: scale.value,\n })\n translateX.value = clampedTranslate.x\n translateY.value = clampedTranslate.y\n })\n .onEnd(e => {\n // Prevents saving pan position if image is zoomed out while panning\n if (scale.value <= DEFAULT_SCALE) return\n\n const clampDecayBounds = clampDecay(scale.value)\n\n translateX.value = withDecay(\n {\n velocity: e.velocityX * DECAY_VELOCITY_FACTOR,\n clamp: clampDecayBounds.x,\n deceleration: 0.996,\n },\n finished => {\n if (finished) {\n savedTranslateX.value = translateX.value\n }\n }\n )\n\n translateY.value = withDecay(\n {\n velocity: e.velocityY * DECAY_VELOCITY_FACTOR,\n clamp: clampDecayBounds.y,\n deceleration: 0.996,\n },\n finished => {\n if (finished) {\n savedTranslateY.value = translateY.value\n }\n }\n )\n })\n\n const panToDismissModalGesture = Gesture.Pan()\n .onUpdate(e => {\n const atDefaultScale = scale.value === DEFAULT_SCALE\n\n // Calculate zoom conditions for dismissing modal\n const atVerticalBoundry = isImageAtVerticalBoundry(scale.value)\n const panDirectionIsPrimarilyVertical = Math.abs(e.translationY) > Math.abs(e.translationX)\n const canDismissWhileZoomed = atVerticalBoundry && panDirectionIsPrimarilyVertical\n\n if (atDefaultScale || canDismissWhileZoomed) {\n const panDistance = Math.abs(e.translationY)\n const fadeProgress = panDistance / DISMISS_PAN_THRESHOLD\n\n opacity.value = Math.max(0, DEFAULT_OPACITY - fadeProgress)\n dismissY.value = e.translationY\n }\n })\n .onEnd(() => {\n const exceededDismissThreshold = Math.abs(dismissY.value) > DISMISS_PAN_THRESHOLD\n\n if (exceededDismissThreshold) {\n runOnJS(handleCloseModal)()\n } else {\n runOnJS(resetDismissGestures)()\n }\n })\n\n /* ==============================\n IMPLEMENT GESTURES & ANIMATIONS\n ================================= */\n // Race between pinch and pan ensures only one is active at a time, preserving focal point logic\n const pinchOrPanGestures = Gesture.Race(pinchGesture, panGesture)\n\n // Exclusive race ensures single tap doesn't interfere with double tap\n const tapGestures = Gesture.Exclusive(doubleTapGesture, singleTapGesture)\n\n // Race between tap gestures and pinch/pan\n const transformImageGestures = Gesture.Race(tapGestures, pinchOrPanGestures)\n\n // Dismiss can work simultaneously with all gestures\n const composedGesture = Gesture.Simultaneous(transformImageGestures, panToDismissModalGesture)\n\n const animatedToolbarStyles = useAnimatedStyle(() => ({\n opacity: toolbarVisible.value,\n transform: [\n { translateY: (1 - toolbarVisible.value) * -20 }, // slide up when hiding\n ],\n }))\n\n return (\n <Modal visible={visible} transparent animationType=\"fade\" onRequestClose={handleCloseModal}>\n <StatusBar\n barStyle=\"light-content\"\n hidden={isStatusBarHidden}\n animated\n showHideTransition=\"slide\"\n />\n <GestureHandlerRootView>\n <GestureDetector gesture={composedGesture}>\n <PreventPressEventsBubbling>\n <FlatList\n data={imageAttachments}\n renderItem={({ item, index }) => (\n <GestureImage\n item={item}\n gesturesEnabled={index === currentImageIndex}\n scale={scale}\n translateX={translateX}\n translateY={translateY}\n dismissY={dismissY}\n opacity={opacity}\n />\n )}\n keyExtractor={(item, index) => `${item.id}-${index}`}\n horizontal\n pagingEnabled\n scrollEnabled={flatListScrollEnabled}\n showsHorizontalScrollIndicator={false}\n initialScrollIndex={initialImageIndex}\n getItemLayout={getItemLayout}\n onMomentumScrollEnd={onMomentumScrollEnd}\n onViewableItemsChanged={onViewableItemsChanged}\n viewabilityConfig={{\n itemVisiblePercentThreshold: 50, // 50% of the image must be visible in the window to be considered viewable\n }}\n style={styles.gallery}\n contentContainerStyle={styles.galleryContentContainer}\n />\n </PreventPressEventsBubbling>\n </GestureDetector>\n </GestureHandlerRootView>\n\n <Animated.View\n style={[styles.actionToolbar, animatedToolbarStyles]}\n accessibilityRole=\"toolbar\"\n >\n <View style={styles.actionToolbarTextMeta}>\n <Heading variant=\"h3\" style={styles.actionToolbarTitle} numberOfLines={1}>\n {authorName}\n </Heading>\n <Text variant=\"tertiary\" style={styles.actionToolbarSubtitle}>\n {formatDatePreview(createdAt)}\n </Text>\n </View>\n <IconButton\n onPress={handleOpenInBrowser}\n name=\"general.newWindow\"\n accessibilityRole=\"link\"\n accessibilityLabel=\"Open image in browser\"\n accessibilityHint=\"Image can be downloaded and shared through the browser.\"\n style={styles.actionButton}\n iconStyle={styles.actionButtonIcon}\n size=\"lg\"\n />\n <IconButton\n onPress={handleCloseModal}\n name=\"general.x\"\n accessibilityLabel=\"Close image\"\n style={styles.actionButton}\n iconStyle={styles.actionButtonIcon}\n size=\"lg\"\n />\n </Animated.View>\n </Modal>\n )\n}\n\nconst PreventPressEventsBubbling = ({ children }: { children: React.ReactNode }) => {\n return (\n <Pressable onLongPress={() => {}} onPress={() => {}}>\n {children}\n </Pressable>\n )\n}\n\ninterface GestureImageProps {\n item: DenormalizedMessageAttachmentResource\n gesturesEnabled: boolean\n scale: SharedValue<number>\n translateX: SharedValue<number>\n translateY: SharedValue<number>\n dismissY: SharedValue<number>\n opacity: SharedValue<number>\n}\n\nconst GestureImage = ({\n item,\n gesturesEnabled,\n scale,\n translateX,\n translateY,\n dismissY,\n opacity,\n}: GestureImageProps) => {\n const styles = useStyles()\n const { url: itemUrl, urlMedium: itemUrlMedium } = item.attributes\n\n const animatedImageStyles = useAnimatedStyle(() => {\n return {\n transform: [\n { translateX: gesturesEnabled ? translateX.value : DEFAULT_TRANSLATE_X },\n { translateY: gesturesEnabled ? translateY.value + dismissY.value : DEFAULT_TRANSLATE_Y },\n { scale: gesturesEnabled ? scale.value : DEFAULT_SCALE },\n ],\n opacity: opacity.value,\n }\n })\n\n return (\n <Image\n source={{ uri: itemUrlMedium || itemUrl }}\n style={styles.gestureImage}\n animatedImageStyle={animatedImageStyles}\n loadingBackgroundStyles={styles.gestureImageLoading}\n resizeMode=\"contain\"\n alt=\"\"\n />\n )\n}\n\ninterface UseStylesProps {\n imageWidth?: number\n imageHeight?: number\n}\n\nconst useStyles = ({ imageWidth = 100, imageHeight = 100 }: UseStylesProps = {}) => {\n const { top, bottom } = useSafeAreaInsets()\n const backgroundColor = tokens.colorNeutral7\n const transparentBackgroundColor = useMemo(\n () => colorFunction(backgroundColor).alpha(0.8).toString(),\n [backgroundColor]\n )\n\n return StyleSheet.create({\n container: {\n maxWidth: '100%',\n },\n attachmentImageWrapper: {\n width: '100%',\n minWidth: 200,\n aspectRatio: imageWidth / imageHeight,\n },\n image: {\n borderRadius: 8,\n },\n gallery: {\n backgroundColor,\n },\n galleryContentContainer: {\n paddingTop: top,\n paddingBottom: bottom,\n },\n gestureImage: {\n height: '100%',\n width: WINDOW_WIDTH,\n backgroundColor: 'transparent',\n },\n gestureImageLoading: {\n backgroundColor,\n },\n actionToolbar: {\n width: '100%',\n position: 'absolute',\n top: 0,\n flexDirection: 'row',\n alignItems: 'center',\n gap: 20,\n paddingHorizontal: 16,\n paddingTop: top + 16,\n paddingBottom: 8,\n backgroundColor: transparentBackgroundColor,\n },\n actionToolbarTextMeta: {\n flex: 1,\n },\n actionToolbarTitle: {\n marginRight: 'auto',\n flexShrink: 1,\n color: tokens.colorNeutral88,\n },\n actionToolbarSubtitle: {\n color: tokens.colorNeutral68,\n },\n actionButton: {\n backgroundColor,\n height: 40,\n width: 40,\n borderRadius: 50,\n borderWidth: 1,\n borderColor: tokens.colorNeutral24,\n },\n actionButtonIcon: {\n color: tokens.colorNeutral88,\n },\n })\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"message_attachments.d.ts","sourceRoot":"","sources":["../../../src/components/conversation/message_attachments.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAEzB,OAAO,EACL,8BAA8B,EAC9B,qCAAqC,EACtC,MAAM,wDAAwD,CAAA;AAM/D,OAAO,EAAyB,KAAK,SAAS,EAAE,MAAM,uCAAuC,CAAA;AAM7F,wBAAgB,kBAAkB,CAAC,KAAK,EAAE;IACxC,WAAW,EAAE,8BAA8B,EAAE,CAAA;IAC7C,SAAS,EAAE,SAAS,CAAA;IACpB,4BAA4B,EAAE,CAAC,UAAU,EAAE,qCAAqC,KAAK,IAAI,CAAA;IACzF,kBAAkB,EAAE,MAAM,IAAI,CAAA;CAC/B,4BA2DA"}
1
+ {"version":3,"file":"message_attachments.d.ts","sourceRoot":"","sources":["../../../src/components/conversation/message_attachments.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAEzB,OAAO,EACL,8BAA8B,EAC9B,qCAAqC,EACtC,MAAM,wDAAwD,CAAA;AAM/D,OAAO,EAAyB,KAAK,SAAS,EAAE,MAAM,uCAAuC,CAAA;AAM7F,wBAAgB,kBAAkB,CAAC,KAAK,EAAE;IACxC,WAAW,EAAE,8BAA8B,EAAE,CAAA;IAC7C,SAAS,EAAE,SAAS,CAAA;IACpB,4BAA4B,EAAE,CAAC,UAAU,EAAE,qCAAqC,KAAK,IAAI,CAAA;IACzF,kBAAkB,EAAE,MAAM,IAAI,CAAA;CAC/B,4BA6DA"}
@@ -19,7 +19,7 @@ export function MessageAttachments(props) {
19
19
  const showImageAttachmentGroup = ENABLE_MESSAGE_ATTACHMENT_IMAGE_GALLERY && imageAttachments.length > 0;
20
20
  return (<View style={styles.attachmentsContainer}>
21
21
  {showImageAttachmentGroup &&
22
- imageAttachments.map((image, index) => (<ImageAttachment key={`${image.id}-${index}`} attachment={image} metaProps={metaProps} onMessageAttachmentLongPress={onMessageAttachmentLongPress}/>))}
22
+ imageAttachments.map((image, index) => (<ImageAttachment key={`${image.id}-${index}`} attachment={image} imageAttachments={imageAttachments} currentImageIndex={index} metaProps={metaProps} onMessageAttachmentLongPress={onMessageAttachmentLongPress}/>))}
23
23
 
24
24
  {attachments.map((attachment, index) => {
25
25
  switch (attachment.type) {
@@ -1 +1 @@
1
- {"version":3,"file":"message_attachments.js","sourceRoot":"","sources":["../../../src/components/conversation/message_attachments.tsx"],"names":[],"mappings":"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","sourcesContent":["import React from 'react'\nimport { View, StyleSheet } from 'react-native'\nimport {\n DenormalizedAttachmentResource,\n DenormalizedMessageAttachmentResource,\n} from '../../types/resources/denormalized_attachment_resource'\nimport { AudioAttachment } from './attachments/audio_attachment'\nimport { VideoAttachment } from './attachments/video_attachment'\nimport { GiphyAttachment } from './attachments/giphy_attachment'\nimport { GenericFileAttachment } from './attachments/generic_file_attachment'\nimport { ExpandedLink } from './attachments/expanded_link'\nimport { ImageAttachmentLegacy, type MetaProps } from './attachments/image_attachment_legacy'\nimport { ImageAttachment } from './attachments/image_attachment'\n\n// Temporarily controls whether image attachments can be opened in a gallery lightbox. (Will remove after QA approves project.)\nconst ENABLE_MESSAGE_ATTACHMENT_IMAGE_GALLERY = false\n\nexport function MessageAttachments(props: {\n attachments: DenormalizedAttachmentResource[]\n metaProps: MetaProps\n onMessageAttachmentLongPress: (attachment: DenormalizedMessageAttachmentResource) => void\n onMessageLongPress: () => void\n}) {\n const styles = useStyles()\n const { attachments, metaProps, onMessageAttachmentLongPress, onMessageLongPress } = props\n if (!attachments || attachments.length === 0) return null\n\n const imageAttachments = attachments.filter(\n attachment =>\n attachment.type === 'MessageAttachment' &&\n attachment.attributes?.contentType?.startsWith('image/')\n ) as DenormalizedMessageAttachmentResource[]\n\n const showImageAttachmentGroup =\n ENABLE_MESSAGE_ATTACHMENT_IMAGE_GALLERY && imageAttachments.length > 0\n\n return (\n <View style={styles.attachmentsContainer}>\n {showImageAttachmentGroup &&\n imageAttachments.map((image, index) => (\n <ImageAttachment\n key={`${image.id}-${index}`}\n attachment={image}\n metaProps={metaProps}\n onMessageAttachmentLongPress={onMessageAttachmentLongPress}\n />\n ))}\n\n {attachments.map((attachment, index) => {\n switch (attachment.type) {\n case 'MessageAttachment':\n return (\n <MessageAttachment\n key={`${attachment.id}-${index}`}\n attachment={attachment}\n metaProps={metaProps}\n onMessageAttachmentLongPress={onMessageAttachmentLongPress}\n />\n )\n case 'giphy':\n return (\n <GiphyAttachment\n key={`${attachment.id || attachment.titleLink}-${index}`}\n attachment={attachment}\n onMessageLongPress={onMessageLongPress}\n />\n )\n case 'ExpandedLink':\n return (\n <ExpandedLink\n key={`${attachment.id}-${index}`}\n attachment={attachment}\n onMessageLongPress={onMessageLongPress}\n />\n )\n default:\n return null\n }\n })}\n </View>\n )\n}\n\nfunction MessageAttachment({\n attachment,\n metaProps,\n onMessageAttachmentLongPress,\n}: {\n attachment: DenormalizedMessageAttachmentResource\n metaProps: MetaProps\n onMessageAttachmentLongPress: (attachment: DenormalizedMessageAttachmentResource) => void\n}) {\n const { attributes } = attachment\n const contentType = attributes?.contentType\n const basicType = contentType ? contentType.split('/')[0] : ''\n\n if (basicType === 'image' && ENABLE_MESSAGE_ATTACHMENT_IMAGE_GALLERY) {\n return null\n }\n\n switch (basicType) {\n case 'image':\n return (\n <ImageAttachmentLegacy\n attachment={attachment}\n metaProps={metaProps}\n onMessageAttachmentLongPress={onMessageAttachmentLongPress}\n />\n )\n case 'video':\n return (\n <VideoAttachment\n attachment={attachment}\n onMessageAttachmentLongPress={onMessageAttachmentLongPress}\n />\n )\n case 'audio':\n return (\n <AudioAttachment\n attachment={attachment}\n onMessageAttachmentLongPress={onMessageAttachmentLongPress}\n />\n )\n default:\n return (\n <GenericFileAttachment\n attachment={attachment}\n onMessageAttachmentLongPress={onMessageAttachmentLongPress}\n />\n )\n }\n}\n\nconst useStyles = () => {\n return StyleSheet.create({\n attachmentsContainer: {\n gap: 2,\n padding: 2,\n },\n })\n}\n"]}
1
+ {"version":3,"file":"message_attachments.js","sourceRoot":"","sources":["../../../src/components/conversation/message_attachments.tsx"],"names":[],"mappings":"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","sourcesContent":["import React from 'react'\nimport { View, StyleSheet } from 'react-native'\nimport {\n DenormalizedAttachmentResource,\n DenormalizedMessageAttachmentResource,\n} from '../../types/resources/denormalized_attachment_resource'\nimport { AudioAttachment } from './attachments/audio_attachment'\nimport { VideoAttachment } from './attachments/video_attachment'\nimport { GiphyAttachment } from './attachments/giphy_attachment'\nimport { GenericFileAttachment } from './attachments/generic_file_attachment'\nimport { ExpandedLink } from './attachments/expanded_link'\nimport { ImageAttachmentLegacy, type MetaProps } from './attachments/image_attachment_legacy'\nimport { ImageAttachment } from './attachments/image_attachment'\n\n// Temporarily controls whether image attachments can be opened in a gallery lightbox. (Will remove after QA approves project.)\nconst ENABLE_MESSAGE_ATTACHMENT_IMAGE_GALLERY = false\n\nexport function MessageAttachments(props: {\n attachments: DenormalizedAttachmentResource[]\n metaProps: MetaProps\n onMessageAttachmentLongPress: (attachment: DenormalizedMessageAttachmentResource) => void\n onMessageLongPress: () => void\n}) {\n const styles = useStyles()\n const { attachments, metaProps, onMessageAttachmentLongPress, onMessageLongPress } = props\n if (!attachments || attachments.length === 0) return null\n\n const imageAttachments = attachments.filter(\n attachment =>\n attachment.type === 'MessageAttachment' &&\n attachment.attributes?.contentType?.startsWith('image/')\n ) as DenormalizedMessageAttachmentResource[]\n\n const showImageAttachmentGroup =\n ENABLE_MESSAGE_ATTACHMENT_IMAGE_GALLERY && imageAttachments.length > 0\n\n return (\n <View style={styles.attachmentsContainer}>\n {showImageAttachmentGroup &&\n imageAttachments.map((image, index) => (\n <ImageAttachment\n key={`${image.id}-${index}`}\n attachment={image}\n imageAttachments={imageAttachments}\n currentImageIndex={index}\n metaProps={metaProps}\n onMessageAttachmentLongPress={onMessageAttachmentLongPress}\n />\n ))}\n\n {attachments.map((attachment, index) => {\n switch (attachment.type) {\n case 'MessageAttachment':\n return (\n <MessageAttachment\n key={`${attachment.id}-${index}`}\n attachment={attachment}\n metaProps={metaProps}\n onMessageAttachmentLongPress={onMessageAttachmentLongPress}\n />\n )\n case 'giphy':\n return (\n <GiphyAttachment\n key={`${attachment.id || attachment.titleLink}-${index}`}\n attachment={attachment}\n onMessageLongPress={onMessageLongPress}\n />\n )\n case 'ExpandedLink':\n return (\n <ExpandedLink\n key={`${attachment.id}-${index}`}\n attachment={attachment}\n onMessageLongPress={onMessageLongPress}\n />\n )\n default:\n return null\n }\n })}\n </View>\n )\n}\n\nfunction MessageAttachment({\n attachment,\n metaProps,\n onMessageAttachmentLongPress,\n}: {\n attachment: DenormalizedMessageAttachmentResource\n metaProps: MetaProps\n onMessageAttachmentLongPress: (attachment: DenormalizedMessageAttachmentResource) => void\n}) {\n const { attributes } = attachment\n const contentType = attributes?.contentType\n const basicType = contentType ? contentType.split('/')[0] : ''\n\n if (basicType === 'image' && ENABLE_MESSAGE_ATTACHMENT_IMAGE_GALLERY) {\n return null\n }\n\n switch (basicType) {\n case 'image':\n return (\n <ImageAttachmentLegacy\n attachment={attachment}\n metaProps={metaProps}\n onMessageAttachmentLongPress={onMessageAttachmentLongPress}\n />\n )\n case 'video':\n return (\n <VideoAttachment\n attachment={attachment}\n onMessageAttachmentLongPress={onMessageAttachmentLongPress}\n />\n )\n case 'audio':\n return (\n <AudioAttachment\n attachment={attachment}\n onMessageAttachmentLongPress={onMessageAttachmentLongPress}\n />\n )\n default:\n return (\n <GenericFileAttachment\n attachment={attachment}\n onMessageAttachmentLongPress={onMessageAttachmentLongPress}\n />\n )\n }\n}\n\nconst useStyles = () => {\n return StyleSheet.create({\n attachmentsContainer: {\n gap: 2,\n padding: 2,\n },\n })\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planningcenter/chat-react-native",
3
- "version": "3.12.0-rc.1",
3
+ "version": "3.12.0-rc.2",
4
4
  "description": "",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -55,5 +55,5 @@
55
55
  "prettier": "^3.4.2",
56
56
  "typescript": "<5.6.0"
57
57
  },
58
- "gitHead": "d48b1c6eb062a8ecd83507fc2beebcddbe95fb2f"
58
+ "gitHead": "ca633e8e42c62de56a0776dd856879ad6440de9e"
59
59
  }
@@ -1,7 +1,19 @@
1
- import React, { useMemo, useState, Dispatch, SetStateAction } from 'react'
2
- import { StatusBar, StyleSheet, Modal, View, Linking, Dimensions, Platform } from 'react-native'
1
+ import React, { useMemo, useState, Dispatch, SetStateAction, useCallback } from 'react'
2
+ import {
3
+ StatusBar,
4
+ StyleSheet,
5
+ Modal,
6
+ View,
7
+ Linking,
8
+ Dimensions,
9
+ Platform,
10
+ NativeSyntheticEvent,
11
+ NativeScrollEvent,
12
+ ViewToken,
13
+ } from 'react-native'
3
14
  import { useSafeAreaInsets } from 'react-native-safe-area-context'
4
15
  import {
16
+ FlatList,
5
17
  Gesture,
6
18
  GestureDetector,
7
19
  GestureHandlerRootView,
@@ -14,6 +26,7 @@ import Animated, {
14
26
  useSharedValue,
15
27
  withSpring,
16
28
  withDecay,
29
+ SharedValue,
17
30
  } from 'react-native-reanimated'
18
31
  import { tokens } from '../../../vendor/tapestry/tokens'
19
32
  import { IconButton, Image, Heading, Text } from '../../display'
@@ -28,6 +41,8 @@ const DISMISS_PAN_THRESHOLD = 300
28
41
  const MIN_DISTANCE_PAN_FOR_PLATFORM = Platform.OS === 'android' ? 5 : 0 // Android requires a higher threshold to give pinching priority
29
42
  const POINTER_FOR_SINGLE_FINGER_PAN = 1 // Single-finger panning helps to avoid conflicts with pinching
30
43
  const DEFAULT_OPACITY = 1
44
+ const DEFAULT_TRANSLATE_X = 0
45
+ const DEFAULT_TRANSLATE_Y = 0
31
46
  const DEFAULT_SCALE = 1
32
47
  const MIN_SCALE = 0.5
33
48
  const MAX_SCALE = 5
@@ -46,10 +61,14 @@ export type MetaProps = {
46
61
 
47
62
  export function ImageAttachment({
48
63
  attachment,
64
+ imageAttachments,
65
+ currentImageIndex,
49
66
  metaProps,
50
67
  onMessageAttachmentLongPress,
51
68
  }: {
52
69
  attachment: DenormalizedMessageAttachmentResource
70
+ imageAttachments: DenormalizedMessageAttachmentResource[]
71
+ currentImageIndex: number
53
72
  metaProps: MetaProps
54
73
  onMessageAttachmentLongPress: (attachment: DenormalizedMessageAttachmentResource) => void
55
74
  }) {
@@ -72,17 +91,16 @@ export function ImageAttachment({
72
91
  <Image
73
92
  source={{ uri: urlMedium || url }}
74
93
  style={styles.image}
75
- wrapperStyle={styles.imageWrapper}
94
+ wrapperStyle={styles.attachmentImageWrapper}
76
95
  alt={filename}
77
96
  />
78
97
  </PlatformPressable>
79
98
  <LightboxModal
80
99
  visible={visible}
81
100
  setModalVisible={setVisible}
82
- uri={urlMedium || url}
101
+ imageAttachments={imageAttachments}
102
+ initialImageIndex={currentImageIndex}
83
103
  metaProps={metaProps}
84
- imageWidth={metadata.width}
85
- imageHeight={metadata.height}
86
104
  />
87
105
  </>
88
106
  )
@@ -91,25 +109,30 @@ export function ImageAttachment({
91
109
  interface LightboxModalProps {
92
110
  visible: boolean
93
111
  setModalVisible: Dispatch<SetStateAction<boolean>>
94
- uri: string
112
+ imageAttachments: DenormalizedMessageAttachmentResource[]
113
+ initialImageIndex: number
95
114
  metaProps: MetaProps
96
- imageWidth?: number
97
- imageHeight?: number
98
115
  }
99
116
 
100
117
  const LightboxModal = ({
101
- uri,
102
118
  visible,
103
119
  setModalVisible,
120
+ imageAttachments,
121
+ initialImageIndex,
104
122
  metaProps,
105
- imageWidth,
106
- imageHeight,
107
123
  }: LightboxModalProps) => {
108
124
  const styles = useStyles()
109
125
  const insets = useSafeAreaInsets()
126
+ const [currentImageIndex, setCurrentImageIndex] = useState(initialImageIndex)
110
127
 
111
128
  const { authorName, createdAt } = metaProps
112
129
 
130
+ // Get current image data
131
+ const currentImage = imageAttachments[currentImageIndex]
132
+ const { url, urlMedium, metadata = {} } = currentImage.attributes
133
+ const imageWidth = metadata.width
134
+ const imageHeight = metadata.height
135
+
113
136
  // Calculate available space for image display
114
137
  const availableWindowWidth = WINDOW_WIDTH
115
138
  const availableWindowHeight = WINDOW_HEIGHT - insets.top - insets.bottom
@@ -123,14 +146,17 @@ const LightboxModal = ({
123
146
  const scale = useSharedValue(DEFAULT_SCALE) // zoom level of image
124
147
  const focalX = useSharedValue(0) // focal point of image between fingers
125
148
  const focalY = useSharedValue(0) // focal point of image between fingers
126
- const translateX = useSharedValue(0) // horizontal distance to pan image
127
- const translateY = useSharedValue(0) // vertical distance to pan image
149
+ const translateX = useSharedValue(DEFAULT_TRANSLATE_X) // horizontal distance to pan image
150
+ const translateY = useSharedValue(DEFAULT_TRANSLATE_Y) // vertical distance to pan image
128
151
  const savedScale = useSharedValue(DEFAULT_SCALE) // previous zoom level
129
- const savedTranslateX = useSharedValue(0) // previous horizontal position
130
- const savedTranslateY = useSharedValue(0) // previous vertical position
152
+ const savedTranslateX = useSharedValue(DEFAULT_TRANSLATE_X) // previous horizontal position
153
+ const savedTranslateY = useSharedValue(DEFAULT_TRANSLATE_Y) // previous vertical position
131
154
  const toolbarVisible = useSharedValue(1) // toolbar visibility state
155
+
132
156
  // React (JS) State:
133
157
  const [isStatusBarHidden, setIsStatusBarHidden] = useState(false)
158
+ const [flatListScrollEnabled, setFlatListScrollEnabled] = useState(true)
159
+ const [panGestureEnabled, setPanGestureEnabled] = useState(false)
134
160
 
135
161
  // Syncs toolbar useSharedValue state with React's state so that the status bar can be hidden/shown based on the toolbar's animation
136
162
  useAnimatedReaction(
@@ -140,37 +166,58 @@ const LightboxModal = ({
140
166
  }
141
167
  )
142
168
 
169
+ // Syncs FlatList scroll state with scale changes
170
+ // When image is at default scale, enable scroll and disable pan gesture
171
+ // When image is zoomed in, disable scroll and enable pan gesture
172
+ useAnimatedReaction(
173
+ () => scale.value,
174
+ value => {
175
+ const enableFlatListScroll = value === DEFAULT_SCALE
176
+ runOnJS(setFlatListScrollEnabled)(enableFlatListScroll)
177
+ runOnJS(setPanGestureEnabled)(!enableFlatListScroll)
178
+ }
179
+ )
180
+
143
181
  /* ============================
144
182
  HANDLERS
145
183
  ============================ */
146
- const handleOpenInBrowser = () => {
147
- Linking.openURL(uri)
148
- }
184
+ const handleOpenInBrowser = useCallback(() => {
185
+ Linking.openURL(urlMedium || url)
186
+ }, [urlMedium, url])
149
187
 
150
- const resetDismissGestures = () => {
188
+ const resetDismissGestures = useCallback(() => {
151
189
  dismissY.value = withSpring(0, RESET_SPRING_CONFIG)
152
190
  opacity.value = withSpring(DEFAULT_OPACITY, RESET_SPRING_CONFIG)
153
- }
191
+ }, [dismissY, opacity])
154
192
 
155
- const resetAllGestures = () => {
193
+ const resetAllGestures = useCallback(() => {
156
194
  resetDismissGestures()
157
195
  scale.value = withSpring(DEFAULT_SCALE, RESET_SPRING_CONFIG)
158
- translateX.value = withSpring(0, RESET_SPRING_CONFIG)
159
- translateY.value = withSpring(0, RESET_SPRING_CONFIG)
196
+ translateX.value = withSpring(DEFAULT_TRANSLATE_X, RESET_SPRING_CONFIG)
197
+ translateY.value = withSpring(DEFAULT_TRANSLATE_Y, RESET_SPRING_CONFIG)
160
198
  savedScale.value = DEFAULT_SCALE
161
- savedTranslateX.value = 0
162
- savedTranslateY.value = 0
199
+ savedTranslateX.value = DEFAULT_TRANSLATE_X
200
+ savedTranslateY.value = DEFAULT_TRANSLATE_Y
163
201
  toolbarVisible.value = withSpring(1, RESET_SPRING_CONFIG)
164
- }
165
-
166
- const handleCloseModal = () => {
202
+ }, [
203
+ resetDismissGestures,
204
+ scale,
205
+ translateX,
206
+ translateY,
207
+ savedScale,
208
+ savedTranslateX,
209
+ savedTranslateY,
210
+ toolbarVisible,
211
+ ])
212
+
213
+ const handleCloseModal = useCallback(() => {
167
214
  setModalVisible(false)
168
215
  resetAllGestures()
169
- }
216
+ }, [setModalVisible, resetAllGestures])
170
217
 
171
218
  /* ============================
172
- UTILITY FUNCTIONS
173
- 'worklet' runs functions on the UI thread, instead of the JS thread for better performance
219
+ UTILITY WORKLET FUNCTIONS
220
+ 'worklet' runs functions on the UI thread, instead of the JS thread for better performance.
174
221
  ============================ */
175
222
  const getImageContainedToWindowDimensions = () => {
176
223
  'worklet'
@@ -248,8 +295,8 @@ const LightboxModal = ({
248
295
  const excessHeight = scaledHeight - availableWindowHeight
249
296
 
250
297
  // How far the image can move in each direction before hitting window edges
251
- const maxTranslateX = Math.max(0, excessWidth / 2)
252
- const maxTranslateY = Math.max(0, excessHeight / 2)
298
+ const maxTranslateX = Math.max(DEFAULT_TRANSLATE_X, excessWidth / 2)
299
+ const maxTranslateY = Math.max(DEFAULT_TRANSLATE_Y, excessHeight / 2)
253
300
 
254
301
  return { maxTranslateX, maxTranslateY }
255
302
  }
@@ -292,7 +339,7 @@ const LightboxModal = ({
292
339
  // Calculate how much the image can exceed the window container
293
340
  const scaledHeight = containedImageHeight * currentScale
294
341
  const excessHeight = scaledHeight - availableWindowHeight
295
- const maxTranslateY = Math.max(0, excessHeight / 2)
342
+ const maxTranslateY = Math.max(DEFAULT_TRANSLATE_Y, excessHeight / 2)
296
343
 
297
344
  const currentTranslateY = translateY.value
298
345
  const panPositionTolerance = 1 // buffer to account for translateY being at a subpixel position
@@ -303,6 +350,57 @@ const LightboxModal = ({
303
350
  return atTopBoundry || atBottomBoundry
304
351
  }
305
352
 
353
+ /* ============================
354
+ UTILITY FLATLIST FUNCTIONS
355
+ Supports the image gallery layout and swipe functionality.
356
+ ============================ */
357
+
358
+ // Used in tandem with FlatList's initialScrollIndex to quickly calculate the position and size of each image before they load.
359
+ const getItemLayout = useCallback(
360
+ (_: unknown, index: number) => ({
361
+ length: WINDOW_WIDTH,
362
+ offset: WINDOW_WIDTH * index,
363
+ index,
364
+ }),
365
+ []
366
+ )
367
+
368
+ // Captures the current image's index after the FlatList finishes its scroll animation.
369
+ // Used in tandem with onViewableItemsChanged to ensure the final value for currentImageIndex is set.
370
+ const onMomentumScrollEnd = useCallback(
371
+ (event: NativeSyntheticEvent<NativeScrollEvent>) => {
372
+ // Calculate the index of the image that is currently visible
373
+ const imageOffsetX = event.nativeEvent.contentOffset.x
374
+ const newImageIndex = Math.round(imageOffsetX / WINDOW_WIDTH)
375
+
376
+ // Check if the image index has changed and the FlatList didn't scroll past the first or last image
377
+ const didImageIndexChange = newImageIndex !== currentImageIndex
378
+ const isImageIndexWithinBounds = newImageIndex >= 0 && newImageIndex < imageAttachments.length
379
+
380
+ if (didImageIndexChange && isImageIndexWithinBounds) {
381
+ setCurrentImageIndex(newImageIndex)
382
+ }
383
+ },
384
+ [currentImageIndex, imageAttachments.length]
385
+ )
386
+
387
+ // Supplements onMomentumScrollEnd by capturing the current image's index while the FlatList is actively scrolling.
388
+ // Used in tandem with viewabilityConfig to trigger when the image is 50% visible in the window.
389
+ const onViewableItemsChanged = useCallback(
390
+ ({ viewableItems }: { viewableItems: ViewToken<DenormalizedMessageAttachmentResource>[] }) => {
391
+ if (viewableItems.length === 0) return
392
+
393
+ // Use the first viewable item which is enforced by the FlatList's pagingEnabled prop that allows only two images to be visible at a time when scrolling.
394
+ const firstViewableItem = viewableItems[0]
395
+ const newIndex = firstViewableItem.index
396
+
397
+ if (newIndex !== null && newIndex !== currentImageIndex) {
398
+ setCurrentImageIndex(newIndex)
399
+ }
400
+ },
401
+ [currentImageIndex]
402
+ )
403
+
306
404
  /* ============================
307
405
  GESTURES
308
406
  ============================ */
@@ -437,6 +535,7 @@ const LightboxModal = ({
437
535
  .minDistance(MIN_DISTANCE_PAN_FOR_PLATFORM)
438
536
  .minPointers(POINTER_FOR_SINGLE_FINGER_PAN)
439
537
  .maxPointers(POINTER_FOR_SINGLE_FINGER_PAN)
538
+ .enabled(panGestureEnabled)
440
539
  .onStart(() => {
441
540
  // Update saved position to current animated position
442
541
  // This ensures smooth continuation if decay is running
@@ -534,15 +633,6 @@ const LightboxModal = ({
534
633
  // Dismiss can work simultaneously with all gestures
535
634
  const composedGesture = Gesture.Simultaneous(transformImageGestures, panToDismissModalGesture)
536
635
 
537
- const animatedImageStyles = useAnimatedStyle(() => ({
538
- transform: [
539
- { translateX: translateX.value },
540
- { translateY: translateY.value + dismissY.value },
541
- { scale: scale.value },
542
- ],
543
- opacity: opacity.value,
544
- }))
545
-
546
636
  const animatedToolbarStyles = useAnimatedStyle(() => ({
547
637
  opacity: toolbarVisible.value,
548
638
  transform: [
@@ -558,23 +648,41 @@ const LightboxModal = ({
558
648
  animated
559
649
  showHideTransition="slide"
560
650
  />
561
- <View style={styles.lightboxModalSafeArea}>
562
- <GestureHandlerRootView>
563
- <GestureDetector gesture={composedGesture}>
564
- <PreventPressEventsBubbling>
565
- <Image
566
- source={{ uri }}
567
- loadingBackgroundStyles={styles.lightboxImageLoading}
568
- style={styles.lightboxImage}
569
- animatedImageStyle={animatedImageStyles}
570
- resizeMode="contain"
571
- animated={true}
572
- alt=""
573
- />
574
- </PreventPressEventsBubbling>
575
- </GestureDetector>
576
- </GestureHandlerRootView>
577
- </View>
651
+ <GestureHandlerRootView>
652
+ <GestureDetector gesture={composedGesture}>
653
+ <PreventPressEventsBubbling>
654
+ <FlatList
655
+ data={imageAttachments}
656
+ renderItem={({ item, index }) => (
657
+ <GestureImage
658
+ item={item}
659
+ gesturesEnabled={index === currentImageIndex}
660
+ scale={scale}
661
+ translateX={translateX}
662
+ translateY={translateY}
663
+ dismissY={dismissY}
664
+ opacity={opacity}
665
+ />
666
+ )}
667
+ keyExtractor={(item, index) => `${item.id}-${index}`}
668
+ horizontal
669
+ pagingEnabled
670
+ scrollEnabled={flatListScrollEnabled}
671
+ showsHorizontalScrollIndicator={false}
672
+ initialScrollIndex={initialImageIndex}
673
+ getItemLayout={getItemLayout}
674
+ onMomentumScrollEnd={onMomentumScrollEnd}
675
+ onViewableItemsChanged={onViewableItemsChanged}
676
+ viewabilityConfig={{
677
+ itemVisiblePercentThreshold: 50, // 50% of the image must be visible in the window to be considered viewable
678
+ }}
679
+ style={styles.gallery}
680
+ contentContainerStyle={styles.galleryContentContainer}
681
+ />
682
+ </PreventPressEventsBubbling>
683
+ </GestureDetector>
684
+ </GestureHandlerRootView>
685
+
578
686
  <Animated.View
579
687
  style={[styles.actionToolbar, animatedToolbarStyles]}
580
688
  accessibilityRole="toolbar"
@@ -618,6 +726,51 @@ const PreventPressEventsBubbling = ({ children }: { children: React.ReactNode })
618
726
  )
619
727
  }
620
728
 
729
+ interface GestureImageProps {
730
+ item: DenormalizedMessageAttachmentResource
731
+ gesturesEnabled: boolean
732
+ scale: SharedValue<number>
733
+ translateX: SharedValue<number>
734
+ translateY: SharedValue<number>
735
+ dismissY: SharedValue<number>
736
+ opacity: SharedValue<number>
737
+ }
738
+
739
+ const GestureImage = ({
740
+ item,
741
+ gesturesEnabled,
742
+ scale,
743
+ translateX,
744
+ translateY,
745
+ dismissY,
746
+ opacity,
747
+ }: GestureImageProps) => {
748
+ const styles = useStyles()
749
+ const { url: itemUrl, urlMedium: itemUrlMedium } = item.attributes
750
+
751
+ const animatedImageStyles = useAnimatedStyle(() => {
752
+ return {
753
+ transform: [
754
+ { translateX: gesturesEnabled ? translateX.value : DEFAULT_TRANSLATE_X },
755
+ { translateY: gesturesEnabled ? translateY.value + dismissY.value : DEFAULT_TRANSLATE_Y },
756
+ { scale: gesturesEnabled ? scale.value : DEFAULT_SCALE },
757
+ ],
758
+ opacity: opacity.value,
759
+ }
760
+ })
761
+
762
+ return (
763
+ <Image
764
+ source={{ uri: itemUrlMedium || itemUrl }}
765
+ style={styles.gestureImage}
766
+ animatedImageStyle={animatedImageStyles}
767
+ loadingBackgroundStyles={styles.gestureImageLoading}
768
+ resizeMode="contain"
769
+ alt=""
770
+ />
771
+ )
772
+ }
773
+
621
774
  interface UseStylesProps {
622
775
  imageWidth?: number
623
776
  imageHeight?: number
@@ -635,7 +788,7 @@ const useStyles = ({ imageWidth = 100, imageHeight = 100 }: UseStylesProps = {})
635
788
  container: {
636
789
  maxWidth: '100%',
637
790
  },
638
- imageWrapper: {
791
+ attachmentImageWrapper: {
639
792
  width: '100%',
640
793
  minWidth: 200,
641
794
  aspectRatio: imageWidth / imageHeight,
@@ -643,20 +796,19 @@ const useStyles = ({ imageWidth = 100, imageHeight = 100 }: UseStylesProps = {})
643
796
  image: {
644
797
  borderRadius: 8,
645
798
  },
646
- lightboxModalSafeArea: {
647
- flex: 1,
799
+ gallery: {
648
800
  backgroundColor,
649
- justifyContent: 'center',
650
- alignItems: 'center',
801
+ },
802
+ galleryContentContainer: {
651
803
  paddingTop: top,
652
804
  paddingBottom: bottom,
653
805
  },
654
- lightboxImage: {
806
+ gestureImage: {
655
807
  height: '100%',
656
808
  width: WINDOW_WIDTH,
657
- backgroundColor,
809
+ backgroundColor: 'transparent',
658
810
  },
659
- lightboxImageLoading: {
811
+ gestureImageLoading: {
660
812
  backgroundColor,
661
813
  },
662
814
  actionToolbar: {
@@ -41,6 +41,8 @@ export function MessageAttachments(props: {
41
41
  <ImageAttachment
42
42
  key={`${image.id}-${index}`}
43
43
  attachment={image}
44
+ imageAttachments={imageAttachments}
45
+ currentImageIndex={index}
44
46
  metaProps={metaProps}
45
47
  onMessageAttachmentLongPress={onMessageAttachmentLongPress}
46
48
  />