@planningcenter/chat-react-native 3.11.0-rc.4 → 3.11.0-rc.5

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.
@@ -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;AAS1E,OAAO,EAAE,qCAAqC,EAAE,MAAM,2DAA2D,CAAA;AAgBjH,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,KAAsD,MAAM,OAAO,CAAA;AAe1E,OAAO,EAAE,qCAAqC,EAAE,MAAM,2DAA2D,CAAA;AAiBjH,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"}
@@ -2,7 +2,7 @@ import React, { useMemo, useState } from 'react';
2
2
  import { StyleSheet, Modal, SafeAreaView, View, Linking, Dimensions } from 'react-native';
3
3
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
4
4
  import { Gesture, GestureDetector, GestureHandlerRootView } from 'react-native-gesture-handler';
5
- import { runOnJS, useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated';
5
+ import { runOnJS, useAnimatedStyle, useSharedValue, withSpring, withDecay, } from 'react-native-reanimated';
6
6
  import { tokens } from '../../../vendor/tapestry/tokens';
7
7
  import { IconButton, Image, Heading, Text } from '../../display';
8
8
  import colorFunction from 'color';
@@ -16,6 +16,7 @@ const DEFAULT_SCALE = 1;
16
16
  const MIN_SCALE = 0.5;
17
17
  const MAX_SCALE = 5;
18
18
  const DOUBLE_TAP_ZOOM_SCALE = 2;
19
+ const DECAY_VELOCITY_FACTOR = 0.4;
19
20
  const RESET_SPRING_CONFIG = {
20
21
  damping: 20,
21
22
  stiffness: 150,
@@ -117,25 +118,31 @@ const LightboxModal = ({ uri, visible, setModalVisible, metaProps, imageWidth, i
117
118
  translateY: newTranslateY,
118
119
  };
119
120
  };
120
- const clampTranslation = ({ x, y, currentScale, allowGestureOvershoot = false, }) => {
121
+ const getMaxTranslationLimits = (currentScale) => {
121
122
  'worklet';
122
123
  const { width: containedImageWidth, height: containedImageHeight } = getImageContainedToWindowDimensions();
123
- // Image dimensions after scaling / zooming
124
+ // Image dimensions after scaling/zooming
124
125
  const scaledWidth = containedImageWidth * currentScale;
125
126
  const scaledHeight = containedImageHeight * currentScale;
126
127
  // How much the scaled image exceeds the window container
127
128
  const excessWidth = scaledWidth - availableWindowWidth;
128
129
  const excessHeight = scaledHeight - availableWindowHeight;
129
- // How far the image can move in each direction
130
+ // How far the image can move in each direction before hitting window edges
130
131
  const maxTranslateX = Math.max(0, excessWidth / 2);
131
132
  const maxTranslateY = Math.max(0, excessHeight / 2);
132
- if (allowGestureOvershoot) {
133
- const overshoot = 20;
134
- return {
135
- x: Math.min(maxTranslateX + overshoot, Math.max(-maxTranslateX - overshoot, x)),
136
- y: Math.min(maxTranslateY + overshoot, Math.max(-maxTranslateY - overshoot, y)),
137
- };
138
- }
133
+ return { maxTranslateX, maxTranslateY };
134
+ };
135
+ const clampDecay = (currentScale) => {
136
+ 'worklet';
137
+ const { maxTranslateX, maxTranslateY } = getMaxTranslationLimits(currentScale);
138
+ return {
139
+ x: [-maxTranslateX, maxTranslateX],
140
+ y: [-maxTranslateY, maxTranslateY],
141
+ };
142
+ };
143
+ const clampTranslation = ({ x, y, currentScale, }) => {
144
+ 'worklet';
145
+ const { maxTranslateX, maxTranslateY } = getMaxTranslationLimits(currentScale);
139
146
  return {
140
147
  x: Math.min(maxTranslateX, Math.max(-maxTranslateX, x)),
141
148
  y: Math.min(maxTranslateY, Math.max(-maxTranslateY, y)),
@@ -194,6 +201,9 @@ const LightboxModal = ({ uri, visible, setModalVisible, metaProps, imageWidth, i
194
201
  .onStart(e => {
195
202
  focalX.value = e.focalX;
196
203
  focalY.value = e.focalY;
204
+ // Ensure that pinch accounts for the decay animation and starts from the true current position.
205
+ savedTranslateX.value = translateX.value;
206
+ savedTranslateY.value = translateY.value;
197
207
  })
198
208
  .onUpdate(e => {
199
209
  // Zoom image in/out within scale limits
@@ -240,6 +250,12 @@ const LightboxModal = ({ uri, visible, setModalVisible, metaProps, imageWidth, i
240
250
  savedTranslateY.value = clampedTranslate.y;
241
251
  });
242
252
  const panGesture = Gesture.Pan()
253
+ .onStart(() => {
254
+ // Update saved position to current animated position
255
+ // This ensures smooth continuation if decay is running
256
+ savedTranslateX.value = translateX.value;
257
+ savedTranslateY.value = translateY.value;
258
+ })
243
259
  .onUpdate(e => {
244
260
  // Only pan if image is zoomed in
245
261
  if (scale.value <= DEFAULT_SCALE)
@@ -251,26 +267,33 @@ const LightboxModal = ({ uri, visible, setModalVisible, metaProps, imageWidth, i
251
267
  x: newTranslateX,
252
268
  y: newTranslateY,
253
269
  currentScale: scale.value,
254
- allowGestureOvershoot: true,
255
270
  });
256
271
  translateX.value = clampedTranslate.x;
257
272
  translateY.value = clampedTranslate.y;
258
273
  })
259
- .onEnd(() => {
274
+ .onEnd(e => {
260
275
  // Prevents saving pan position if image is zoomed out while panning
261
276
  if (scale.value <= DEFAULT_SCALE)
262
277
  return;
263
- // Spring image back to window boundaries if overshot
264
- const clampedTranslate = clampTranslation({
265
- x: translateX.value,
266
- y: translateY.value,
267
- currentScale: scale.value,
278
+ const clampDecayBounds = clampDecay(scale.value);
279
+ translateX.value = withDecay({
280
+ velocity: e.velocityX * DECAY_VELOCITY_FACTOR,
281
+ clamp: clampDecayBounds.x,
282
+ deceleration: 0.996,
283
+ }, finished => {
284
+ if (finished) {
285
+ savedTranslateX.value = translateX.value;
286
+ }
287
+ });
288
+ translateY.value = withDecay({
289
+ velocity: e.velocityY * DECAY_VELOCITY_FACTOR,
290
+ clamp: clampDecayBounds.y,
291
+ deceleration: 0.996,
292
+ }, finished => {
293
+ if (finished) {
294
+ savedTranslateY.value = translateY.value;
295
+ }
268
296
  });
269
- translateX.value = withSpring(clampedTranslate.x, RESET_SPRING_CONFIG);
270
- translateY.value = withSpring(clampedTranslate.y, RESET_SPRING_CONFIG);
271
- // Save the current position for the next gesture
272
- savedTranslateX.value = clampedTranslate.x;
273
- savedTranslateY.value = clampedTranslate.y;
274
297
  });
275
298
  const panToDismissModalGesture = Gesture.Pan()
276
299
  .onUpdate(e => {
@@ -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 { StyleSheet, Modal, SafeAreaView, View, Linking, Dimensions } from 'react-native'\nimport { useSafeAreaInsets } from 'react-native-safe-area-context'\nimport { Gesture, GestureDetector, GestureHandlerRootView } from 'react-native-gesture-handler'\nimport { runOnJS, useAnimatedStyle, useSharedValue, withSpring } 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 DEFAULT_OPACITY = 1\nconst DEFAULT_SCALE = 1\nconst MIN_SCALE = 0.5\nconst MAX_SCALE = 5\nconst DOUBLE_TAP_ZOOM_SCALE = 2\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 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\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 }\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 clampTranslation = ({\n x,\n y,\n currentScale,\n allowGestureOvershoot = false,\n }: {\n x: number\n y: number\n currentScale: number\n allowGestureOvershoot?: boolean\n }) => {\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\n const maxTranslateX = Math.max(0, excessWidth / 2)\n const maxTranslateY = Math.max(0, excessHeight / 2)\n\n if (allowGestureOvershoot) {\n const overshoot = 20\n return {\n x: Math.min(maxTranslateX + overshoot, Math.max(-maxTranslateX - overshoot, x)),\n y: Math.min(maxTranslateY + overshoot, Math.max(-maxTranslateY - overshoot, y)),\n }\n }\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 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 // 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 .onUpdate(e => {\n // Zoom image in/out within scale limits\n const newScale = savedScale.value * e.scale\n const newScaleClamped = Math.min(MAX_SCALE, Math.max(MIN_SCALE, newScale))\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 // Ensure image edges remain within window boundaries\n const clampedTranslate = clampTranslation({\n x: translateX.value,\n y: translateY.value,\n currentScale: currentScale,\n })\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 = currentScale\n savedTranslateX.value = clampedTranslate.x\n savedTranslateY.value = clampedTranslate.y\n })\n\n const panGesture = Gesture.Pan()\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 allowGestureOvershoot: true,\n })\n translateX.value = clampedTranslate.x\n translateY.value = clampedTranslate.y\n })\n .onEnd(() => {\n // Prevents saving pan position if image is zoomed out while panning\n if (scale.value <= DEFAULT_SCALE) return\n\n // Spring image back to window boundaries if overshot\n const clampedTranslate = clampTranslation({\n x: translateX.value,\n y: translateY.value,\n currentScale: scale.value,\n })\n translateX.value = withSpring(clampedTranslate.x, RESET_SPRING_CONFIG)\n translateY.value = withSpring(clampedTranslate.y, RESET_SPRING_CONFIG)\n\n // Save the current position for the next gesture\n savedTranslateX.value = clampedTranslate.x\n savedTranslateY.value = clampedTranslate.y\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, preventing issues with zooming to focal point between fingers\n const pinchOrPanGestures = Gesture.Race(pinchGesture, panGesture)\n\n // Race between double-tap and pinch/pan ensures that pinch/pan can't interrupt double-tap\n const transformImageGestures = Gesture.Race(doubleTapGesture, 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 return (\n <Modal visible={visible} transparent animationType=\"fade\" onRequestClose={handleCloseModal}>\n <SafeAreaView style={styles.modal}>\n <GestureHandlerRootView>\n <GestureDetector gesture={composedGesture}>\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 </GestureDetector>\n <View style={styles.actionToolbar} accessibilityRole=\"toolbar\">\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 />\n </View>\n </GestureHandlerRootView>\n </SafeAreaView>\n </Modal>\n )\n}\n\ninterface UseStylesProps {\n imageWidth?: number\n imageHeight?: number\n}\n\nconst useStyles = ({ imageWidth = 100, imageHeight = 100 }: UseStylesProps = {}) => {\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 modal: {\n flex: 1,\n backgroundColor,\n justifyContent: 'center',\n alignItems: 'center',\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: 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":"AAAA,OAAO,KAAK,EAAE,EAAE,OAAO,EAAE,QAAQ,EAA4B,MAAM,OAAO,CAAA;AAC1E,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AACzF,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAA;AAClE,OAAO,EAAE,OAAO,EAAE,eAAe,EAAE,sBAAsB,EAAE,MAAM,8BAA8B,CAAA;AAC/F,OAAO,EACL,OAAO,EACP,gBAAgB,EAChB,cAAc,EACd,UAAU,EACV,SAAS,GACV,MAAM,yBAAyB,CAAA;AAChC,OAAO,EAAE,MAAM,EAAE,MAAM,iCAAiC,CAAA;AACxD,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,eAAe,CAAA;AAChE,OAAO,aAAa,MAAM,OAAO,CAAA;AACjC,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AAEvD,OAAO,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAA;AAC9D,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAEzC,MAAM,EAAE,KAAK,EAAE,YAAY,EAAE,MAAM,EAAE,aAAa,EAAE,GAAG,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;AAC/E,MAAM,qBAAqB,GAAG,GAAG,CAAA;AACjC,MAAM,eAAe,GAAG,CAAC,CAAA;AACzB,MAAM,aAAa,GAAG,CAAC,CAAA;AACvB,MAAM,SAAS,GAAG,GAAG,CAAA;AACrB,MAAM,SAAS,GAAG,CAAC,CAAA;AACnB,MAAM,qBAAqB,GAAG,CAAC,CAAA;AAC/B,MAAM,qBAAqB,GAAG,GAAG,CAAA;AACjC,MAAM,mBAAmB,GAAG;IAC1B,OAAO,EAAE,EAAE;IACX,SAAS,EAAE,GAAG;CACf,CAAA;AAOD,MAAM,UAAU,eAAe,CAAC,EAC9B,UAAU,EACV,SAAS,EACT,4BAA4B,GAK7B;IACC,MAAM,EAAE,UAAU,EAAE,GAAG,UAAU,CAAA;IACjC,MAAM,EAAE,GAAG,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,GAAG,EAAE,EAAE,GAAG,UAAU,CAAA;IAC9D,MAAM,EAAE,MAAM,EAAE,GAAG,QAAQ,EAAE,CAAA;IAE7B,MAAM,MAAM,GAAG,SAAS,CAAC,EAAE,UAAU,EAAE,QAAQ,CAAC,KAAK,EAAE,WAAW,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAA;IACtF,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;IAE7C,OAAO,CACL,EACE;MAAA,CAAC,iBAAiB,CAChB,KAAK,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,CACxB,OAAO,CAAC,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAChC,WAAW,CAAC,CAAC,GAAG,EAAE,CAAC,4BAA4B,CAAC,UAAU,CAAC,CAAC,CAC5D,cAAc,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,oBAAoB,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CACzE,iBAAiB,CAAC,6BAA6B,CAE/C;QAAA,CAAC,KAAK,CACJ,MAAM,CAAC,CAAC,EAAE,GAAG,EAAE,SAAS,IAAI,GAAG,EAAE,CAAC,CAClC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CACpB,YAAY,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC,CAClC,GAAG,CAAC,CAAC,QAAQ,CAAC,EAElB;MAAA,EAAE,iBAAiB,CACnB;MAAA,CAAC,aAAa,CACZ,OAAO,CAAC,CAAC,OAAO,CAAC,CACjB,eAAe,CAAC,CAAC,UAAU,CAAC,CAC5B,GAAG,CAAC,CAAC,SAAS,IAAI,GAAG,CAAC,CACtB,SAAS,CAAC,CAAC,SAAS,CAAC,CACrB,UAAU,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAC3B,WAAW,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,EAEjC;IAAA,GAAG,CACJ,CAAA;AACH,CAAC;AAWD,MAAM,aAAa,GAAG,CAAC,EACrB,GAAG,EACH,OAAO,EACP,eAAe,EACf,SAAS,EACT,UAAU,EACV,WAAW,GACQ,EAAE,EAAE;IACvB,MAAM,MAAM,GAAG,SAAS,EAAE,CAAA;IAC1B,MAAM,MAAM,GAAG,iBAAiB,EAAE,CAAA;IAElC,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,GAAG,SAAS,CAAA;IAE3C,8CAA8C;IAC9C,MAAM,oBAAoB,GAAG,YAAY,CAAA;IACzC,MAAM,qBAAqB,GAAG,aAAa,GAAG,MAAM,CAAC,GAAG,GAAG,MAAM,CAAC,MAAM,CAAA;IAExE;;mCAE+B;IAC/B,MAAM,QAAQ,GAAG,cAAc,CAAC,CAAC,CAAC,CAAA,CAAC,qCAAqC;IACxE,MAAM,OAAO,GAAG,cAAc,CAAC,eAAe,CAAC,CAAA,CAAC,mBAAmB;IACnE,MAAM,KAAK,GAAG,cAAc,CAAC,aAAa,CAAC,CAAA,CAAC,sBAAsB;IAClE,MAAM,MAAM,GAAG,cAAc,CAAC,CAAC,CAAC,CAAA,CAAC,uCAAuC;IACxE,MAAM,MAAM,GAAG,cAAc,CAAC,CAAC,CAAC,CAAA,CAAC,uCAAuC;IACxE,MAAM,UAAU,GAAG,cAAc,CAAC,CAAC,CAAC,CAAA,CAAC,mCAAmC;IACxE,MAAM,UAAU,GAAG,cAAc,CAAC,CAAC,CAAC,CAAA,CAAC,iCAAiC;IACtE,MAAM,UAAU,GAAG,cAAc,CAAC,aAAa,CAAC,CAAA,CAAC,sBAAsB;IACvE,MAAM,eAAe,GAAG,cAAc,CAAC,CAAC,CAAC,CAAA,CAAC,+BAA+B;IACzE,MAAM,eAAe,GAAG,cAAc,CAAC,CAAC,CAAC,CAAA,CAAC,6BAA6B;IAEvE;;mCAE+B;IAC/B,MAAM,mBAAmB,GAAG,GAAG,EAAE;QAC/B,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;IACtB,CAAC,CAAA;IAED,MAAM,oBAAoB,GAAG,GAAG,EAAE;QAChC,QAAQ,CAAC,KAAK,GAAG,UAAU,CAAC,CAAC,EAAE,mBAAmB,CAAC,CAAA;QACnD,OAAO,CAAC,KAAK,GAAG,UAAU,CAAC,eAAe,EAAE,mBAAmB,CAAC,CAAA;IAClE,CAAC,CAAA;IAED,MAAM,gBAAgB,GAAG,GAAG,EAAE;QAC5B,oBAAoB,EAAE,CAAA;QACtB,KAAK,CAAC,KAAK,GAAG,UAAU,CAAC,aAAa,EAAE,mBAAmB,CAAC,CAAA;QAC5D,UAAU,CAAC,KAAK,GAAG,UAAU,CAAC,CAAC,EAAE,mBAAmB,CAAC,CAAA;QACrD,UAAU,CAAC,KAAK,GAAG,UAAU,CAAC,CAAC,EAAE,mBAAmB,CAAC,CAAA;QACrD,UAAU,CAAC,KAAK,GAAG,aAAa,CAAA;QAChC,eAAe,CAAC,KAAK,GAAG,CAAC,CAAA;QACzB,eAAe,CAAC,KAAK,GAAG,CAAC,CAAA;IAC3B,CAAC,CAAA;IAED,MAAM,gBAAgB,GAAG,GAAG,EAAE;QAC5B,eAAe,CAAC,KAAK,CAAC,CAAA;QACtB,gBAAgB,EAAE,CAAA;IACpB,CAAC,CAAA;IAED;;;mCAG+B;IAC/B,MAAM,mCAAmC,GAAG,GAAG,EAAE;QAC/C,SAAS,CAAA;QAET,IAAI,CAAC,UAAU,IAAI,CAAC,WAAW,EAAE,CAAC;YAChC,OAAO,EAAE,KAAK,EAAE,oBAAoB,EAAE,MAAM,EAAE,qBAAqB,EAAE,CAAA;QACvE,CAAC;QAED,MAAM,gBAAgB,GAAG,UAAU,GAAG,WAAW,CAAA;QACjD,MAAM,iBAAiB,GAAG,oBAAoB,GAAG,qBAAqB,CAAA;QAEtE,iDAAiD;QACjD,IAAI,gBAAgB,GAAG,iBAAiB,EAAE,CAAC;YACzC,OAAO;gBACL,KAAK,EAAE,oBAAoB;gBAC3B,MAAM,EAAE,oBAAoB,GAAG,gBAAgB;aAChD,CAAA;QACH,CAAC;QAED,mDAAmD;QACnD,OAAO;YACL,KAAK,EAAE,qBAAqB,GAAG,gBAAgB;YAC/C,MAAM,EAAE,qBAAqB;SAC9B,CAAA;IACH,CAAC,CAAA;IAED,MAAM,gBAAgB,GAAG,CAAC,EACxB,WAAW,EACX,WAAW,EACX,WAAW,EACX,iBAAiB,EACjB,sBAAsB,EACtB,sBAAsB,GAQvB,EAAE,EAAE;QACH,SAAS,CAAA;QAET,2DAA2D;QAC3D,MAAM,aAAa,GAAG,YAAY,GAAG,CAAC,CAAA;QACtC,MAAM,aAAa,GAAG,aAAa,GAAG,CAAC,CAAA;QAEvC,6DAA6D;QAC7D,MAAM,cAAc,GAAG,WAAW,GAAG,aAAa,GAAG,sBAAsB,CAAA;QAC3E,MAAM,cAAc,GAAG,WAAW,GAAG,aAAa,GAAG,sBAAsB,CAAA;QAE3E,8DAA8D;QAC9D,MAAM,UAAU,GAAG,WAAW,GAAG,iBAAiB,CAAA;QAClD,MAAM,aAAa,GAAG,sBAAsB,GAAG,cAAc,GAAG,CAAC,CAAC,GAAG,UAAU,CAAC,CAAA;QAChF,MAAM,aAAa,GAAG,sBAAsB,GAAG,cAAc,GAAG,CAAC,CAAC,GAAG,UAAU,CAAC,CAAA;QAEhF,OAAO;YACL,UAAU,EAAE,aAAa;YACzB,UAAU,EAAE,aAAa;SAC1B,CAAA;IACH,CAAC,CAAA;IAED,MAAM,uBAAuB,GAAG,CAAC,YAAoB,EAAE,EAAE;QACvD,SAAS,CAAA;QAET,MAAM,EAAE,KAAK,EAAE,mBAAmB,EAAE,MAAM,EAAE,oBAAoB,EAAE,GAChE,mCAAmC,EAAE,CAAA;QAEvC,yCAAyC;QACzC,MAAM,WAAW,GAAG,mBAAmB,GAAG,YAAY,CAAA;QACtD,MAAM,YAAY,GAAG,oBAAoB,GAAG,YAAY,CAAA;QAExD,yDAAyD;QACzD,MAAM,WAAW,GAAG,WAAW,GAAG,oBAAoB,CAAA;QACtD,MAAM,YAAY,GAAG,YAAY,GAAG,qBAAqB,CAAA;QAEzD,2EAA2E;QAC3E,MAAM,aAAa,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,WAAW,GAAG,CAAC,CAAC,CAAA;QAClD,MAAM,aAAa,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,YAAY,GAAG,CAAC,CAAC,CAAA;QAEnD,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,CAAA;IACzC,CAAC,CAAA;IAED,MAAM,UAAU,GAAG,CAAC,YAAoB,EAAgD,EAAE;QACxF,SAAS,CAAA;QAET,MAAM,EAAE,aAAa,EAAE,aAAa,EAAE,GAAG,uBAAuB,CAAC,YAAY,CAAC,CAAA;QAE9E,OAAO;YACL,CAAC,EAAE,CAAC,CAAC,aAAa,EAAE,aAAa,CAAC;YAClC,CAAC,EAAE,CAAC,CAAC,aAAa,EAAE,aAAa,CAAC;SACnC,CAAA;IACH,CAAC,CAAA;IAED,MAAM,gBAAgB,GAAG,CAAC,EACxB,CAAC,EACD,CAAC,EACD,YAAY,GAKb,EAAE,EAAE;QACH,SAAS,CAAA;QAET,MAAM,EAAE,aAAa,EAAE,aAAa,EAAE,GAAG,uBAAuB,CAAC,YAAY,CAAC,CAAA;QAE9E,OAAO;YACL,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,aAAa,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC;YACvD,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,aAAa,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC;SACxD,CAAA;IACH,CAAC,CAAA;IAED,MAAM,wBAAwB,GAAG,CAAC,YAAoB,EAAE,EAAE;QACxD,SAAS,CAAA;QAET,MAAM,EAAE,MAAM,EAAE,oBAAoB,EAAE,GAAG,mCAAmC,EAAE,CAAA;QAE9E,+DAA+D;QAC/D,MAAM,YAAY,GAAG,oBAAoB,GAAG,YAAY,CAAA;QACxD,MAAM,YAAY,GAAG,YAAY,GAAG,qBAAqB,CAAA;QACzD,MAAM,aAAa,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,YAAY,GAAG,CAAC,CAAC,CAAA;QAEnD,MAAM,iBAAiB,GAAG,UAAU,CAAC,KAAK,CAAA;QAC1C,MAAM,oBAAoB,GAAG,CAAC,CAAA,CAAC,gEAAgE;QAE/F,MAAM,YAAY,GAAG,iBAAiB,IAAI,aAAa,GAAG,oBAAoB,CAAA;QAC9E,MAAM,eAAe,GAAG,iBAAiB,IAAI,CAAC,aAAa,GAAG,oBAAoB,CAAA;QAElF,OAAO,YAAY,IAAI,eAAe,CAAA;IACxC,CAAC,CAAA;IAED;;mCAE+B;IAC/B,MAAM,gBAAgB,GAAG,OAAO,CAAC,GAAG,EAAE;SACnC,YAAY,CAAC,CAAC,CAAC;SACf,OAAO,CAAC,CAAC,CAAC,EAAE;QACX,MAAM,UAAU,GAAG,KAAK,CAAC,KAAK,GAAG,aAAa,CAAA;QAC9C,IAAI,UAAU,EAAE,CAAC;YACf,OAAO,CAAC,gBAAgB,CAAC,EAAE,CAAA;QAC7B,CAAC;aAAM,CAAC;YACN,6BAA6B;YAC7B,MAAM,cAAc,GAAG,gBAAgB,CAAC;gBACtC,WAAW,EAAE,CAAC,CAAC,CAAC;gBAChB,WAAW,EAAE,CAAC,CAAC,CAAC;gBAChB,WAAW,EAAE,qBAAqB;gBAClC,iBAAiB,EAAE,UAAU,CAAC,KAAK;gBACnC,sBAAsB,EAAE,eAAe,CAAC,KAAK;gBAC7C,sBAAsB,EAAE,eAAe,CAAC,KAAK;aAC9C,CAAC,CAAA;YAEF,uEAAuE;YACvE,MAAM,gBAAgB,GAAG,gBAAgB,CAAC;gBACxC,CAAC,EAAE,cAAc,CAAC,UAAU;gBAC5B,CAAC,EAAE,cAAc,CAAC,UAAU;gBAC5B,YAAY,EAAE,qBAAqB;aACpC,CAAC,CAAA;YAEF,uCAAuC;YACvC,KAAK,CAAC,KAAK,GAAG,UAAU,CAAC,qBAAqB,EAAE,mBAAmB,CAAC,CAAA;YACpE,UAAU,CAAC,KAAK,GAAG,UAAU,CAAC,gBAAgB,CAAC,CAAC,EAAE,mBAAmB,CAAC,CAAA;YACtE,UAAU,CAAC,KAAK,GAAG,UAAU,CAAC,gBAAgB,CAAC,CAAC,EAAE,mBAAmB,CAAC,CAAA;YAEtE,sCAAsC;YACtC,UAAU,CAAC,KAAK,GAAG,qBAAqB,CAAA;YACxC,eAAe,CAAC,KAAK,GAAG,gBAAgB,CAAC,CAAC,CAAA;YAC1C,eAAe,CAAC,KAAK,GAAG,gBAAgB,CAAC,CAAC,CAAA;QAC5C,CAAC;IACH,CAAC,CAAC,CAAA;IAEJ,MAAM,YAAY,GAAG,OAAO,CAAC,KAAK,EAAE;SACjC,OAAO,CAAC,CAAC,CAAC,EAAE;QACX,MAAM,CAAC,KAAK,GAAG,CAAC,CAAC,MAAM,CAAA;QACvB,MAAM,CAAC,KAAK,GAAG,CAAC,CAAC,MAAM,CAAA;QAEvB,gGAAgG;QAChG,eAAe,CAAC,KAAK,GAAG,UAAU,CAAC,KAAK,CAAA;QACxC,eAAe,CAAC,KAAK,GAAG,UAAU,CAAC,KAAK,CAAA;IAC1C,CAAC,CAAC;SACD,QAAQ,CAAC,CAAC,CAAC,EAAE;QACZ,wCAAwC;QACxC,MAAM,QAAQ,GAAG,UAAU,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAA;QAC3C,MAAM,eAAe,GAAG,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC,CAAA;QAC1E,KAAK,CAAC,KAAK,GAAG,eAAe,CAAA;QAE7B,8DAA8D;QAC9D,MAAM,cAAc,GAAG,gBAAgB,CAAC;YACtC,WAAW,EAAE,MAAM,CAAC,KAAK;YACzB,WAAW,EAAE,MAAM,CAAC,KAAK;YACzB,WAAW,EAAE,eAAe;YAC5B,iBAAiB,EAAE,UAAU,CAAC,KAAK;YACnC,sBAAsB,EAAE,eAAe,CAAC,KAAK;YAC7C,sBAAsB,EAAE,eAAe,CAAC,KAAK;SAC9C,CAAC,CAAA;QAEF,uFAAuF;QACvF,UAAU,CAAC,KAAK,GAAG,cAAc,CAAC,UAAU,CAAA;QAC5C,UAAU,CAAC,KAAK,GAAG,cAAc,CAAC,UAAU,CAAA;IAC9C,CAAC,CAAC;SACD,KAAK,CAAC,GAAG,EAAE;QACV,MAAM,YAAY,GAAG,KAAK,CAAC,KAAK,CAAA;QAEhC,oCAAoC;QACpC,IAAI,YAAY,IAAI,SAAS,EAAE,CAAC;YAC9B,OAAO,CAAC,gBAAgB,CAAC,EAAE,CAAA;YAC3B,OAAM;QACR,CAAC;QAED,oDAAoD;QACpD,MAAM,kBAAkB,GAAG,YAAY,IAAI,aAAa,GAAG,GAAG,CAAA;QAC9D,IAAI,kBAAkB,EAAE,CAAC;YACvB,OAAO,CAAC,gBAAgB,CAAC,EAAE,CAAA;YAC3B,OAAM;QACR,CAAC;QAED,qDAAqD;QACrD,MAAM,gBAAgB,GAAG,gBAAgB,CAAC;YACxC,CAAC,EAAE,UAAU,CAAC,KAAK;YACnB,CAAC,EAAE,UAAU,CAAC,KAAK;YACnB,YAAY,EAAE,YAAY;SAC3B,CAAC,CAAA;QACF,UAAU,CAAC,KAAK,GAAG,UAAU,CAAC,gBAAgB,CAAC,CAAC,EAAE,mBAAmB,CAAC,CAAA;QACtE,UAAU,CAAC,KAAK,GAAG,UAAU,CAAC,gBAAgB,CAAC,CAAC,EAAE,mBAAmB,CAAC,CAAA;QAEtE,8BAA8B;QAC9B,UAAU,CAAC,KAAK,GAAG,YAAY,CAAA;QAC/B,eAAe,CAAC,KAAK,GAAG,gBAAgB,CAAC,CAAC,CAAA;QAC1C,eAAe,CAAC,KAAK,GAAG,gBAAgB,CAAC,CAAC,CAAA;IAC5C,CAAC,CAAC,CAAA;IAEJ,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,EAAE;SAC7B,OAAO,CAAC,GAAG,EAAE;QACZ,qDAAqD;QACrD,uDAAuD;QACvD,eAAe,CAAC,KAAK,GAAG,UAAU,CAAC,KAAK,CAAA;QACxC,eAAe,CAAC,KAAK,GAAG,UAAU,CAAC,KAAK,CAAA;IAC1C,CAAC,CAAC;SACD,QAAQ,CAAC,CAAC,CAAC,EAAE;QACZ,iCAAiC;QACjC,IAAI,KAAK,CAAC,KAAK,IAAI,aAAa;YAAE,OAAM;QAExC,MAAM,aAAa,GAAG,eAAe,CAAC,KAAK,GAAG,CAAC,CAAC,YAAY,CAAA;QAC5D,MAAM,aAAa,GAAG,eAAe,CAAC,KAAK,GAAG,CAAC,CAAC,YAAY,CAAA;QAE5D,qDAAqD;QACrD,MAAM,gBAAgB,GAAG,gBAAgB,CAAC;YACxC,CAAC,EAAE,aAAa;YAChB,CAAC,EAAE,aAAa;YAChB,YAAY,EAAE,KAAK,CAAC,KAAK;SAC1B,CAAC,CAAA;QACF,UAAU,CAAC,KAAK,GAAG,gBAAgB,CAAC,CAAC,CAAA;QACrC,UAAU,CAAC,KAAK,GAAG,gBAAgB,CAAC,CAAC,CAAA;IACvC,CAAC,CAAC;SACD,KAAK,CAAC,CAAC,CAAC,EAAE;QACT,oEAAoE;QACpE,IAAI,KAAK,CAAC,KAAK,IAAI,aAAa;YAAE,OAAM;QAExC,MAAM,gBAAgB,GAAG,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;QAEhD,UAAU,CAAC,KAAK,GAAG,SAAS,CAC1B;YACE,QAAQ,EAAE,CAAC,CAAC,SAAS,GAAG,qBAAqB;YAC7C,KAAK,EAAE,gBAAgB,CAAC,CAAC;YACzB,YAAY,EAAE,KAAK;SACpB,EACD,QAAQ,CAAC,EAAE;YACT,IAAI,QAAQ,EAAE,CAAC;gBACb,eAAe,CAAC,KAAK,GAAG,UAAU,CAAC,KAAK,CAAA;YAC1C,CAAC;QACH,CAAC,CACF,CAAA;QAED,UAAU,CAAC,KAAK,GAAG,SAAS,CAC1B;YACE,QAAQ,EAAE,CAAC,CAAC,SAAS,GAAG,qBAAqB;YAC7C,KAAK,EAAE,gBAAgB,CAAC,CAAC;YACzB,YAAY,EAAE,KAAK;SACpB,EACD,QAAQ,CAAC,EAAE;YACT,IAAI,QAAQ,EAAE,CAAC;gBACb,eAAe,CAAC,KAAK,GAAG,UAAU,CAAC,KAAK,CAAA;YAC1C,CAAC;QACH,CAAC,CACF,CAAA;IACH,CAAC,CAAC,CAAA;IAEJ,MAAM,wBAAwB,GAAG,OAAO,CAAC,GAAG,EAAE;SAC3C,QAAQ,CAAC,CAAC,CAAC,EAAE;QACZ,MAAM,cAAc,GAAG,KAAK,CAAC,KAAK,KAAK,aAAa,CAAA;QAEpD,iDAAiD;QACjD,MAAM,iBAAiB,GAAG,wBAAwB,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;QAC/D,MAAM,+BAA+B,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,YAAY,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,YAAY,CAAC,CAAA;QAC3F,MAAM,qBAAqB,GAAG,iBAAiB,IAAI,+BAA+B,CAAA;QAElF,IAAI,cAAc,IAAI,qBAAqB,EAAE,CAAC;YAC5C,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,YAAY,CAAC,CAAA;YAC5C,MAAM,YAAY,GAAG,WAAW,GAAG,qBAAqB,CAAA;YAExD,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,eAAe,GAAG,YAAY,CAAC,CAAA;YAC3D,QAAQ,CAAC,KAAK,GAAG,CAAC,CAAC,YAAY,CAAA;QACjC,CAAC;IACH,CAAC,CAAC;SACD,KAAK,CAAC,GAAG,EAAE;QACV,MAAM,wBAAwB,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,qBAAqB,CAAA;QAEjF,IAAI,wBAAwB,EAAE,CAAC;YAC7B,OAAO,CAAC,gBAAgB,CAAC,EAAE,CAAA;QAC7B,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,oBAAoB,CAAC,EAAE,CAAA;QACjC,CAAC;IACH,CAAC,CAAC,CAAA;IAEJ;;wCAEoC;IACpC,iIAAiI;IACjI,MAAM,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,YAAY,EAAE,UAAU,CAAC,CAAA;IAEjE,0FAA0F;IAC1F,MAAM,sBAAsB,GAAG,OAAO,CAAC,IAAI,CAAC,gBAAgB,EAAE,kBAAkB,CAAC,CAAA;IAEjF,oDAAoD;IACpD,MAAM,eAAe,GAAG,OAAO,CAAC,YAAY,CAAC,sBAAsB,EAAE,wBAAwB,CAAC,CAAA;IAE9F,MAAM,mBAAmB,GAAG,gBAAgB,CAAC,GAAG,EAAE,CAAC,CAAC;QAClD,SAAS,EAAE;YACT,EAAE,UAAU,EAAE,UAAU,CAAC,KAAK,EAAE;YAChC,EAAE,UAAU,EAAE,UAAU,CAAC,KAAK,GAAG,QAAQ,CAAC,KAAK,EAAE;YACjD,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE;SACvB;QACD,OAAO,EAAE,OAAO,CAAC,KAAK;KACvB,CAAC,CAAC,CAAA;IAEH,OAAO,CACL,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,CAAC,WAAW,CAAC,aAAa,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC,gBAAgB,CAAC,CACzF;MAAA,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAChC;QAAA,CAAC,sBAAsB,CACrB;UAAA,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC,eAAe,CAAC,CACxC;YAAA,CAAC,KAAK,CACJ,MAAM,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,CAChB,uBAAuB,CAAC,CAAC,MAAM,CAAC,oBAAoB,CAAC,CACrD,KAAK,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC,CAC5B,kBAAkB,CAAC,CAAC,mBAAmB,CAAC,CACxC,UAAU,CAAC,SAAS,CACpB,QAAQ,CAAC,CAAC,IAAI,CAAC,CACf,GAAG,CAAC,EAAE,EAEV;UAAA,EAAE,eAAe,CACjB;UAAA,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,iBAAiB,CAAC,SAAS,CAC5D;YAAA,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,qBAAqB,CAAC,CACxC;cAAA,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,kBAAkB,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CACvE;gBAAA,CAAC,UAAU,CACb;cAAA,EAAE,OAAO,CACT;cAAA,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,qBAAqB,CAAC,CAC3D;gBAAA,CAAC,iBAAiB,CAAC,SAAS,CAAC,CAC/B;cAAA,EAAE,IAAI,CACR;YAAA,EAAE,IAAI,CACN;YAAA,CAAC,UAAU,CACT,OAAO,CAAC,CAAC,mBAAmB,CAAC,CAC7B,IAAI,CAAC,mBAAmB,CACxB,iBAAiB,CAAC,MAAM,CACxB,kBAAkB,CAAC,uBAAuB,CAC1C,iBAAiB,CAAC,yDAAyD,CAC3E,KAAK,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC,CAC3B,SAAS,CAAC,CAAC,MAAM,CAAC,gBAAgB,CAAC,CACnC,IAAI,CAAC,IAAI,EAEX;YAAA,CAAC,UAAU,CACT,OAAO,CAAC,CAAC,gBAAgB,CAAC,CAC1B,IAAI,CAAC,WAAW,CAChB,kBAAkB,CAAC,aAAa,CAChC,KAAK,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC,CAC3B,SAAS,CAAC,CAAC,MAAM,CAAC,gBAAgB,CAAC,EAEvC;UAAA,EAAE,IAAI,CACR;QAAA,EAAE,sBAAsB,CAC1B;MAAA,EAAE,YAAY,CAChB;IAAA,EAAE,KAAK,CAAC,CACT,CAAA;AACH,CAAC,CAAA;AAOD,MAAM,SAAS,GAAG,CAAC,EAAE,UAAU,GAAG,GAAG,EAAE,WAAW,GAAG,GAAG,KAAqB,EAAE,EAAE,EAAE;IACjF,MAAM,eAAe,GAAG,MAAM,CAAC,aAAa,CAAA;IAC5C,MAAM,0BAA0B,GAAG,OAAO,CACxC,GAAG,EAAE,CAAC,aAAa,CAAC,eAAe,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAC1D,CAAC,eAAe,CAAC,CAClB,CAAA;IAED,OAAO,UAAU,CAAC,MAAM,CAAC;QACvB,SAAS,EAAE;YACT,QAAQ,EAAE,MAAM;SACjB;QACD,YAAY,EAAE;YACZ,KAAK,EAAE,MAAM;YACb,QAAQ,EAAE,GAAG;YACb,WAAW,EAAE,UAAU,GAAG,WAAW;SACtC;QACD,KAAK,EAAE;YACL,YAAY,EAAE,CAAC;SAChB;QACD,KAAK,EAAE;YACL,IAAI,EAAE,CAAC;YACP,eAAe;YACf,cAAc,EAAE,QAAQ;YACxB,UAAU,EAAE,QAAQ;SACrB;QACD,aAAa,EAAE;YACb,MAAM,EAAE,MAAM;YACd,KAAK,EAAE,YAAY;YACnB,eAAe;SAChB;QACD,oBAAoB,EAAE;YACpB,eAAe;SAChB;QACD,aAAa,EAAE;YACb,KAAK,EAAE,MAAM;YACb,QAAQ,EAAE,UAAU;YACpB,GAAG,EAAE,CAAC;YACN,aAAa,EAAE,KAAK;YACpB,UAAU,EAAE,QAAQ;YACpB,GAAG,EAAE,EAAE;YACP,iBAAiB,EAAE,EAAE;YACrB,UAAU,EAAE,EAAE;YACd,aAAa,EAAE,CAAC;YAChB,eAAe,EAAE,0BAA0B;SAC5C;QACD,qBAAqB,EAAE;YACrB,IAAI,EAAE,CAAC;SACR;QACD,kBAAkB,EAAE;YAClB,WAAW,EAAE,MAAM;YACnB,UAAU,EAAE,CAAC;YACb,KAAK,EAAE,MAAM,CAAC,cAAc;SAC7B;QACD,qBAAqB,EAAE;YACrB,KAAK,EAAE,MAAM,CAAC,cAAc;SAC7B;QACD,YAAY,EAAE;YACZ,eAAe;YACf,MAAM,EAAE,EAAE;YACV,KAAK,EAAE,EAAE;YACT,YAAY,EAAE,EAAE;YAChB,WAAW,EAAE,CAAC;YACd,WAAW,EAAE,MAAM,CAAC,cAAc;SACnC;QACD,gBAAgB,EAAE;YAChB,KAAK,EAAE,MAAM,CAAC,cAAc;SAC7B;KACF,CAAC,CAAA;AACJ,CAAC,CAAA","sourcesContent":["import React, { useMemo, useState, Dispatch, SetStateAction } from 'react'\nimport { StyleSheet, Modal, SafeAreaView, View, Linking, Dimensions } from 'react-native'\nimport { useSafeAreaInsets } from 'react-native-safe-area-context'\nimport { Gesture, GestureDetector, GestureHandlerRootView } from 'react-native-gesture-handler'\nimport {\n runOnJS,\n useAnimatedStyle,\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 DEFAULT_OPACITY = 1\nconst DEFAULT_SCALE = 1\nconst MIN_SCALE = 0.5\nconst MAX_SCALE = 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 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\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 }\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 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 // 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 // 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(MAX_SCALE, Math.max(MIN_SCALE, newScale))\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 // Ensure image edges remain within window boundaries\n const clampedTranslate = clampTranslation({\n x: translateX.value,\n y: translateY.value,\n currentScale: currentScale,\n })\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 = currentScale\n savedTranslateX.value = clampedTranslate.x\n savedTranslateY.value = clampedTranslate.y\n })\n\n const panGesture = Gesture.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, preventing issues with zooming to focal point between fingers\n const pinchOrPanGestures = Gesture.Race(pinchGesture, panGesture)\n\n // Race between double-tap and pinch/pan ensures that pinch/pan can't interrupt double-tap\n const transformImageGestures = Gesture.Race(doubleTapGesture, 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 return (\n <Modal visible={visible} transparent animationType=\"fade\" onRequestClose={handleCloseModal}>\n <SafeAreaView style={styles.modal}>\n <GestureHandlerRootView>\n <GestureDetector gesture={composedGesture}>\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 </GestureDetector>\n <View style={styles.actionToolbar} accessibilityRole=\"toolbar\">\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 />\n </View>\n </GestureHandlerRootView>\n </SafeAreaView>\n </Modal>\n )\n}\n\ninterface UseStylesProps {\n imageWidth?: number\n imageHeight?: number\n}\n\nconst useStyles = ({ imageWidth = 100, imageHeight = 100 }: UseStylesProps = {}) => {\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 modal: {\n flex: 1,\n backgroundColor,\n justifyContent: 'center',\n alignItems: 'center',\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: 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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planningcenter/chat-react-native",
3
- "version": "3.11.0-rc.4",
3
+ "version": "3.11.0-rc.5",
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": "070a5b47b238e467d37ee3721fcf618e68bc3301"
58
+ "gitHead": "31aa0b42b7b95950012c8d3be2a818baad5b1dc9"
59
59
  }
@@ -2,7 +2,13 @@ import React, { useMemo, useState, Dispatch, SetStateAction } from 'react'
2
2
  import { StyleSheet, Modal, SafeAreaView, View, Linking, Dimensions } from 'react-native'
3
3
  import { useSafeAreaInsets } from 'react-native-safe-area-context'
4
4
  import { Gesture, GestureDetector, GestureHandlerRootView } from 'react-native-gesture-handler'
5
- import { runOnJS, useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated'
5
+ import {
6
+ runOnJS,
7
+ useAnimatedStyle,
8
+ useSharedValue,
9
+ withSpring,
10
+ withDecay,
11
+ } from 'react-native-reanimated'
6
12
  import { tokens } from '../../../vendor/tapestry/tokens'
7
13
  import { IconButton, Image, Heading, Text } from '../../display'
8
14
  import colorFunction from 'color'
@@ -18,6 +24,7 @@ const DEFAULT_SCALE = 1
18
24
  const MIN_SCALE = 0.5
19
25
  const MAX_SCALE = 5
20
26
  const DOUBLE_TAP_ZOOM_SCALE = 2
27
+ const DECAY_VELOCITY_FACTOR = 0.4
21
28
  const RESET_SPRING_CONFIG = {
22
29
  damping: 20,
23
30
  stiffness: 150,
@@ -204,23 +211,13 @@ const LightboxModal = ({
204
211
  }
205
212
  }
206
213
 
207
- const clampTranslation = ({
208
- x,
209
- y,
210
- currentScale,
211
- allowGestureOvershoot = false,
212
- }: {
213
- x: number
214
- y: number
215
- currentScale: number
216
- allowGestureOvershoot?: boolean
217
- }) => {
214
+ const getMaxTranslationLimits = (currentScale: number) => {
218
215
  'worklet'
219
216
 
220
217
  const { width: containedImageWidth, height: containedImageHeight } =
221
218
  getImageContainedToWindowDimensions()
222
219
 
223
- // Image dimensions after scaling / zooming
220
+ // Image dimensions after scaling/zooming
224
221
  const scaledWidth = containedImageWidth * currentScale
225
222
  const scaledHeight = containedImageHeight * currentScale
226
223
 
@@ -228,17 +225,36 @@ const LightboxModal = ({
228
225
  const excessWidth = scaledWidth - availableWindowWidth
229
226
  const excessHeight = scaledHeight - availableWindowHeight
230
227
 
231
- // How far the image can move in each direction
228
+ // How far the image can move in each direction before hitting window edges
232
229
  const maxTranslateX = Math.max(0, excessWidth / 2)
233
230
  const maxTranslateY = Math.max(0, excessHeight / 2)
234
231
 
235
- if (allowGestureOvershoot) {
236
- const overshoot = 20
237
- return {
238
- x: Math.min(maxTranslateX + overshoot, Math.max(-maxTranslateX - overshoot, x)),
239
- y: Math.min(maxTranslateY + overshoot, Math.max(-maxTranslateY - overshoot, y)),
240
- }
232
+ return { maxTranslateX, maxTranslateY }
233
+ }
234
+
235
+ const clampDecay = (currentScale: number): { x: [number, number]; y: [number, number] } => {
236
+ 'worklet'
237
+
238
+ const { maxTranslateX, maxTranslateY } = getMaxTranslationLimits(currentScale)
239
+
240
+ return {
241
+ x: [-maxTranslateX, maxTranslateX],
242
+ y: [-maxTranslateY, maxTranslateY],
241
243
  }
244
+ }
245
+
246
+ const clampTranslation = ({
247
+ x,
248
+ y,
249
+ currentScale,
250
+ }: {
251
+ x: number
252
+ y: number
253
+ currentScale: number
254
+ }) => {
255
+ 'worklet'
256
+
257
+ const { maxTranslateX, maxTranslateY } = getMaxTranslationLimits(currentScale)
242
258
 
243
259
  return {
244
260
  x: Math.min(maxTranslateX, Math.max(-maxTranslateX, x)),
@@ -308,6 +324,10 @@ const LightboxModal = ({
308
324
  .onStart(e => {
309
325
  focalX.value = e.focalX
310
326
  focalY.value = e.focalY
327
+
328
+ // Ensure that pinch accounts for the decay animation and starts from the true current position.
329
+ savedTranslateX.value = translateX.value
330
+ savedTranslateY.value = translateY.value
311
331
  })
312
332
  .onUpdate(e => {
313
333
  // Zoom image in/out within scale limits
@@ -361,6 +381,12 @@ const LightboxModal = ({
361
381
  })
362
382
 
363
383
  const panGesture = Gesture.Pan()
384
+ .onStart(() => {
385
+ // Update saved position to current animated position
386
+ // This ensures smooth continuation if decay is running
387
+ savedTranslateX.value = translateX.value
388
+ savedTranslateY.value = translateY.value
389
+ })
364
390
  .onUpdate(e => {
365
391
  // Only pan if image is zoomed in
366
392
  if (scale.value <= DEFAULT_SCALE) return
@@ -373,27 +399,41 @@ const LightboxModal = ({
373
399
  x: newTranslateX,
374
400
  y: newTranslateY,
375
401
  currentScale: scale.value,
376
- allowGestureOvershoot: true,
377
402
  })
378
403
  translateX.value = clampedTranslate.x
379
404
  translateY.value = clampedTranslate.y
380
405
  })
381
- .onEnd(() => {
406
+ .onEnd(e => {
382
407
  // Prevents saving pan position if image is zoomed out while panning
383
408
  if (scale.value <= DEFAULT_SCALE) return
384
409
 
385
- // Spring image back to window boundaries if overshot
386
- const clampedTranslate = clampTranslation({
387
- x: translateX.value,
388
- y: translateY.value,
389
- currentScale: scale.value,
390
- })
391
- translateX.value = withSpring(clampedTranslate.x, RESET_SPRING_CONFIG)
392
- translateY.value = withSpring(clampedTranslate.y, RESET_SPRING_CONFIG)
393
-
394
- // Save the current position for the next gesture
395
- savedTranslateX.value = clampedTranslate.x
396
- savedTranslateY.value = clampedTranslate.y
410
+ const clampDecayBounds = clampDecay(scale.value)
411
+
412
+ translateX.value = withDecay(
413
+ {
414
+ velocity: e.velocityX * DECAY_VELOCITY_FACTOR,
415
+ clamp: clampDecayBounds.x,
416
+ deceleration: 0.996,
417
+ },
418
+ finished => {
419
+ if (finished) {
420
+ savedTranslateX.value = translateX.value
421
+ }
422
+ }
423
+ )
424
+
425
+ translateY.value = withDecay(
426
+ {
427
+ velocity: e.velocityY * DECAY_VELOCITY_FACTOR,
428
+ clamp: clampDecayBounds.y,
429
+ deceleration: 0.996,
430
+ },
431
+ finished => {
432
+ if (finished) {
433
+ savedTranslateY.value = translateY.value
434
+ }
435
+ }
436
+ )
397
437
  })
398
438
 
399
439
  const panToDismissModalGesture = Gesture.Pan()