mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 817 lines 23 kB view raw
1/** 2 * Copyright (c) JOB TODAY S.A. and its affiliates. 3 * 4 * This source code is licensed under the MIT license found in the 5 * LICENSE file in the root directory of this source tree. 6 * 7 */ 8// Original code copied and simplified from the link below as the codebase is currently not maintained: 9// https://github.com/jobtoday/react-native-image-viewing 10 11import React, {useCallback, useEffect, useMemo, useState} from 'react' 12import {LayoutAnimation, PixelRatio, StyleSheet, View} from 'react-native' 13import {SystemBars} from 'react-native-edge-to-edge' 14import {Gesture} from 'react-native-gesture-handler' 15import PagerView from 'react-native-pager-view' 16import Animated, { 17 type AnimatedRef, 18 cancelAnimation, 19 interpolate, 20 measure, 21 runOnJS, 22 type SharedValue, 23 useAnimatedReaction, 24 useAnimatedRef, 25 useAnimatedStyle, 26 useDerivedValue, 27 useSharedValue, 28 withDecay, 29 withSpring, 30 type WithSpringConfig, 31} from 'react-native-reanimated' 32import { 33 SafeAreaView, 34 useSafeAreaFrame, 35 useSafeAreaInsets, 36} from 'react-native-safe-area-context' 37import * as ScreenOrientation from 'expo-screen-orientation' 38import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 39import {Trans} from '@lingui/macro' 40 41import {type Dimensions} from '#/lib/media/types' 42import {colors, s} from '#/lib/styles' 43import {isIOS} from '#/platform/detection' 44import {type Lightbox} from '#/state/lightbox' 45import {Button} from '#/view/com/util/forms/Button' 46import {Text} from '#/view/com/util/text/Text' 47import {ScrollView} from '#/view/com/util/Views' 48import {useTheme} from '#/alf' 49import {setSystemUITheme} from '#/alf/util/systemUI' 50import {PlatformInfo} from '../../../../../modules/expo-bluesky-swiss-army' 51import {type ImageSource, type Transform} from './@types' 52import ImageDefaultHeader from './components/ImageDefaultHeader' 53import ImageItem from './components/ImageItem/ImageItem' 54 55type Rect = {x: number; y: number; width: number; height: number} 56 57const PORTRAIT_UP = ScreenOrientation.OrientationLock.PORTRAIT_UP 58const PIXEL_RATIO = PixelRatio.get() 59 60const SLOW_SPRING: WithSpringConfig = { 61 mass: isIOS ? 1.25 : 0.75, 62 damping: 300, 63 stiffness: 800, 64 restDisplacementThreshold: 0.01, 65} 66const FAST_SPRING: WithSpringConfig = { 67 mass: isIOS ? 1.25 : 0.75, 68 damping: 150, 69 stiffness: 900, 70 restDisplacementThreshold: 0.01, 71} 72 73function canAnimate(lightbox: Lightbox): boolean { 74 return ( 75 !PlatformInfo.getIsReducedMotionEnabled() && 76 lightbox.images.every( 77 img => img.thumbRect && (img.dimensions || img.thumbDimensions), 78 ) 79 ) 80} 81 82export default function ImageViewRoot({ 83 lightbox: nextLightbox, 84 onRequestClose, 85 onPressSave, 86 onPressShare, 87}: { 88 lightbox: Lightbox | null 89 onRequestClose: () => void 90 onPressSave: (uri: string) => void 91 onPressShare: (uri: string) => void 92}) { 93 'use no memo' 94 const ref = useAnimatedRef<View>() 95 const [activeLightbox, setActiveLightbox] = useState(nextLightbox) 96 const [orientation, setOrientation] = useState<'portrait' | 'landscape'>( 97 'portrait', 98 ) 99 const openProgress = useSharedValue(0) 100 101 if (!activeLightbox && nextLightbox) { 102 setActiveLightbox(nextLightbox) 103 } 104 105 React.useEffect(() => { 106 if (!nextLightbox) { 107 return 108 } 109 110 const isAnimated = canAnimate(nextLightbox) 111 112 // https://github.com/software-mansion/react-native-reanimated/issues/6677 113 rAF_FIXED(() => { 114 openProgress.set(() => 115 isAnimated ? withClampedSpring(1, SLOW_SPRING) : 1, 116 ) 117 }) 118 return () => { 119 // https://github.com/software-mansion/react-native-reanimated/issues/6677 120 rAF_FIXED(() => { 121 openProgress.set(() => 122 isAnimated ? withClampedSpring(0, SLOW_SPRING) : 0, 123 ) 124 }) 125 } 126 }, [nextLightbox, openProgress]) 127 128 useAnimatedReaction( 129 () => openProgress.get() === 0, 130 (isGone, wasGone) => { 131 if (isGone && !wasGone) { 132 runOnJS(setActiveLightbox)(null) 133 } 134 }, 135 ) 136 137 // Delay the unlock until after we've finished the scale up animation. 138 // It's complicated to do the same for locking it back so we don't attempt that. 139 useAnimatedReaction( 140 () => openProgress.get() === 1, 141 (isOpen, wasOpen) => { 142 if (isOpen && !wasOpen) { 143 runOnJS(ScreenOrientation.unlockAsync)() 144 } else if (!isOpen && wasOpen) { 145 // default is PORTRAIT_UP - set via config plugin in app.config.js -sfn 146 runOnJS(ScreenOrientation.lockAsync)(PORTRAIT_UP) 147 } 148 }, 149 ) 150 151 const onFlyAway = React.useCallback(() => { 152 'worklet' 153 openProgress.set(0) 154 runOnJS(onRequestClose)() 155 }, [onRequestClose, openProgress]) 156 157 return ( 158 // Keep it always mounted to avoid flicker on the first frame. 159 <View 160 style={[styles.screen, !activeLightbox && styles.screenHidden]} 161 aria-modal 162 accessibilityViewIsModal 163 aria-hidden={!activeLightbox}> 164 <Animated.View 165 ref={ref} 166 style={{flex: 1}} 167 collapsable={false} 168 onLayout={e => { 169 const layout = e.nativeEvent.layout 170 setOrientation( 171 layout.height > layout.width ? 'portrait' : 'landscape', 172 ) 173 }}> 174 {activeLightbox && ( 175 <ImageView 176 key={activeLightbox.id + '-' + orientation} 177 lightbox={activeLightbox} 178 orientation={orientation} 179 onRequestClose={onRequestClose} 180 onPressSave={onPressSave} 181 onPressShare={onPressShare} 182 onFlyAway={onFlyAway} 183 safeAreaRef={ref} 184 openProgress={openProgress} 185 /> 186 )} 187 </Animated.View> 188 </View> 189 ) 190} 191 192function ImageView({ 193 lightbox, 194 orientation, 195 onRequestClose, 196 onPressSave, 197 onPressShare, 198 onFlyAway, 199 safeAreaRef, 200 openProgress, 201}: { 202 lightbox: Lightbox 203 orientation: 'portrait' | 'landscape' 204 onRequestClose: () => void 205 onPressSave: (uri: string) => void 206 onPressShare: (uri: string) => void 207 onFlyAway: () => void 208 safeAreaRef: AnimatedRef<View> 209 openProgress: SharedValue<number> 210}) { 211 const {images, index: initialImageIndex} = lightbox 212 const isAnimated = useMemo(() => canAnimate(lightbox), [lightbox]) 213 const [isScaled, setIsScaled] = useState(false) 214 const [isDragging, setIsDragging] = useState(false) 215 const [imageIndex, setImageIndex] = useState(initialImageIndex) 216 const [showControls, setShowControls] = useState(true) 217 const [isAltExpanded, setAltExpanded] = React.useState(false) 218 const dismissSwipeTranslateY = useSharedValue(0) 219 const isFlyingAway = useSharedValue(false) 220 221 const containerStyle = useAnimatedStyle(() => { 222 if (openProgress.get() < 1) { 223 return { 224 pointerEvents: 'none', 225 opacity: isAnimated ? 1 : 0, 226 } 227 } 228 if (isFlyingAway.get()) { 229 return { 230 pointerEvents: 'none', 231 opacity: 1, 232 } 233 } 234 return {pointerEvents: 'auto', opacity: 1} 235 }) 236 237 const backdropStyle = useAnimatedStyle(() => { 238 const screenSize = measure(safeAreaRef) 239 let opacity = 1 240 const openProgressValue = openProgress.get() 241 if (openProgressValue < 1) { 242 opacity = Math.sqrt(openProgressValue) 243 } else if (screenSize && orientation === 'portrait') { 244 const dragProgress = Math.min( 245 Math.abs(dismissSwipeTranslateY.get()) / (screenSize.height / 2), 246 1, 247 ) 248 opacity -= dragProgress 249 } 250 const factor = isIOS ? 100 : 50 251 return { 252 opacity: Math.round(opacity * factor) / factor, 253 } 254 }) 255 256 const animatedHeaderStyle = useAnimatedStyle(() => { 257 const show = showControls && dismissSwipeTranslateY.get() === 0 258 return { 259 pointerEvents: show ? 'box-none' : 'none', 260 opacity: withClampedSpring( 261 show && openProgress.get() === 1 ? 1 : 0, 262 FAST_SPRING, 263 ), 264 transform: [ 265 { 266 translateY: withClampedSpring(show ? 0 : -30, FAST_SPRING), 267 }, 268 ], 269 } 270 }) 271 const animatedFooterStyle = useAnimatedStyle(() => { 272 const show = showControls && dismissSwipeTranslateY.get() === 0 273 return { 274 flexGrow: 1, 275 pointerEvents: show ? 'box-none' : 'none', 276 opacity: withClampedSpring( 277 show && openProgress.get() === 1 ? 1 : 0, 278 FAST_SPRING, 279 ), 280 transform: [ 281 { 282 translateY: withClampedSpring(show ? 0 : 30, FAST_SPRING), 283 }, 284 ], 285 } 286 }) 287 288 const onTap = useCallback(() => { 289 setShowControls(show => !show) 290 }, []) 291 292 const onZoom = useCallback((nextIsScaled: boolean) => { 293 setIsScaled(nextIsScaled) 294 if (nextIsScaled) { 295 setShowControls(false) 296 } 297 }, []) 298 299 useAnimatedReaction( 300 () => { 301 const screenSize = measure(safeAreaRef) 302 return ( 303 !screenSize || 304 Math.abs(dismissSwipeTranslateY.get()) > screenSize.height 305 ) 306 }, 307 (isOut, wasOut) => { 308 if (isOut && !wasOut) { 309 // Stop the animation from blocking the screen forever. 310 cancelAnimation(dismissSwipeTranslateY) 311 onFlyAway() 312 } 313 }, 314 ) 315 316 // style system ui on android 317 const t = useTheme() 318 useEffect(() => { 319 setSystemUITheme('lightbox', t) 320 return () => { 321 setSystemUITheme('theme', t) 322 } 323 }, [t]) 324 325 return ( 326 <Animated.View style={[styles.container, containerStyle]}> 327 <SystemBars 328 style={{statusBar: 'light', navigationBar: 'light'}} 329 hidden={{ 330 statusBar: isScaled || !showControls, 331 navigationBar: false, 332 }} 333 /> 334 <Animated.View 335 style={[styles.backdrop, backdropStyle]} 336 renderToHardwareTextureAndroid 337 /> 338 <PagerView 339 scrollEnabled={!isScaled} 340 initialPage={initialImageIndex} 341 onPageSelected={e => { 342 setImageIndex(e.nativeEvent.position) 343 setIsScaled(false) 344 }} 345 onPageScrollStateChanged={e => { 346 setIsDragging(e.nativeEvent.pageScrollState !== 'idle') 347 }} 348 overdrag={true} 349 style={styles.pager}> 350 {images.map((imageSrc, i) => ( 351 <View key={imageSrc.uri}> 352 <LightboxImage 353 onTap={onTap} 354 onZoom={onZoom} 355 imageSrc={imageSrc} 356 onRequestClose={onRequestClose} 357 isScrollViewBeingDragged={isDragging} 358 showControls={showControls} 359 safeAreaRef={safeAreaRef} 360 isScaled={isScaled} 361 isFlyingAway={isFlyingAway} 362 isActive={i === imageIndex} 363 dismissSwipeTranslateY={dismissSwipeTranslateY} 364 openProgress={openProgress} 365 /> 366 </View> 367 ))} 368 </PagerView> 369 <View style={styles.controls}> 370 <Animated.View 371 style={animatedHeaderStyle} 372 renderToHardwareTextureAndroid> 373 <ImageDefaultHeader onRequestClose={onRequestClose} /> 374 </Animated.View> 375 <Animated.View 376 style={animatedFooterStyle} 377 renderToHardwareTextureAndroid={!isAltExpanded}> 378 <LightboxFooter 379 images={images} 380 index={imageIndex} 381 isAltExpanded={isAltExpanded} 382 toggleAltExpanded={() => setAltExpanded(e => !e)} 383 onPressSave={onPressSave} 384 onPressShare={onPressShare} 385 /> 386 </Animated.View> 387 </View> 388 </Animated.View> 389 ) 390} 391 392function LightboxImage({ 393 imageSrc, 394 onTap, 395 onZoom, 396 onRequestClose, 397 isScrollViewBeingDragged, 398 isScaled, 399 isFlyingAway, 400 isActive, 401 showControls, 402 safeAreaRef, 403 openProgress, 404 dismissSwipeTranslateY, 405}: { 406 imageSrc: ImageSource 407 onRequestClose: () => void 408 onTap: () => void 409 onZoom: (scaled: boolean) => void 410 isScrollViewBeingDragged: boolean 411 isScaled: boolean 412 isActive: boolean 413 isFlyingAway: SharedValue<boolean> 414 showControls: boolean 415 safeAreaRef: AnimatedRef<View> 416 openProgress: SharedValue<number> 417 dismissSwipeTranslateY: SharedValue<number> 418}) { 419 const [fetchedDims, setFetchedDims] = React.useState<Dimensions | null>(null) 420 const dims = fetchedDims ?? imageSrc.dimensions ?? imageSrc.thumbDimensions 421 let imageAspect: number | undefined 422 if (dims) { 423 imageAspect = dims.width / dims.height 424 if (Number.isNaN(imageAspect)) { 425 imageAspect = undefined 426 } 427 } 428 429 const safeFrameDelayedForJSThreadOnly = useSafeAreaFrame() 430 const safeInsetsDelayedForJSThreadOnly = useSafeAreaInsets() 431 const measureSafeArea = React.useCallback(() => { 432 'worklet' 433 let safeArea: Rect | null = measure(safeAreaRef) 434 if (!safeArea) { 435 if (_WORKLET) { 436 console.error('Expected to always be able to measure safe area.') 437 } 438 const frame = safeFrameDelayedForJSThreadOnly 439 const insets = safeInsetsDelayedForJSThreadOnly 440 safeArea = { 441 x: frame.x + insets.left, 442 y: frame.y + insets.top, 443 width: frame.width - insets.left - insets.right, 444 height: frame.height - insets.top - insets.bottom, 445 } 446 } 447 return safeArea 448 }, [ 449 safeFrameDelayedForJSThreadOnly, 450 safeInsetsDelayedForJSThreadOnly, 451 safeAreaRef, 452 ]) 453 454 const {thumbRect} = imageSrc 455 const transforms = useDerivedValue(() => { 456 'worklet' 457 const safeArea = measureSafeArea() 458 const openProgressValue = openProgress.get() 459 const dismissTranslateY = 460 isActive && openProgressValue === 1 ? dismissSwipeTranslateY.get() : 0 461 462 if (openProgressValue === 0 && isFlyingAway.get()) { 463 return { 464 isHidden: true, 465 isResting: false, 466 scaleAndMoveTransform: [], 467 cropFrameTransform: [], 468 cropContentTransform: [], 469 } 470 } 471 472 if (isActive && thumbRect && imageAspect && openProgressValue < 1) { 473 return interpolateTransform( 474 openProgressValue, 475 thumbRect, 476 safeArea, 477 imageAspect, 478 ) 479 } 480 return { 481 isHidden: false, 482 isResting: dismissTranslateY === 0, 483 scaleAndMoveTransform: [{translateY: dismissTranslateY}], 484 cropFrameTransform: [], 485 cropContentTransform: [], 486 } 487 }) 488 489 const dismissSwipePan = Gesture.Pan() 490 .enabled(isActive && !isScaled) 491 .activeOffsetY([-10, 10]) 492 .failOffsetX([-10, 10]) 493 .maxPointers(1) 494 .onUpdate(e => { 495 'worklet' 496 if (openProgress.get() !== 1 || isFlyingAway.get()) { 497 return 498 } 499 dismissSwipeTranslateY.set(e.translationY) 500 }) 501 .onEnd(e => { 502 'worklet' 503 if (openProgress.get() !== 1 || isFlyingAway.get()) { 504 return 505 } 506 if (Math.abs(e.velocityY) > 200) { 507 isFlyingAway.set(true) 508 if (dismissSwipeTranslateY.get() === 0) { 509 // HACK: If the initial value is 0, withDecay() animation doesn't start. 510 // This is a bug in Reanimated, but for now we'll work around it like this. 511 dismissSwipeTranslateY.set(1) 512 } 513 dismissSwipeTranslateY.set(() => { 514 'worklet' 515 return withDecay({ 516 velocity: e.velocityY, 517 velocityFactor: Math.max(3500 / Math.abs(e.velocityY), 1), // Speed up if it's too slow. 518 deceleration: 1, // Danger! This relies on the reaction below stopping it. 519 }) 520 }) 521 } else { 522 dismissSwipeTranslateY.set(() => { 523 'worklet' 524 return withSpring(0, { 525 stiffness: 700, 526 damping: 50, 527 }) 528 }) 529 } 530 }) 531 532 return ( 533 <ImageItem 534 imageSrc={imageSrc} 535 onTap={onTap} 536 onZoom={onZoom} 537 onRequestClose={onRequestClose} 538 onLoad={setFetchedDims} 539 isScrollViewBeingDragged={isScrollViewBeingDragged} 540 showControls={showControls} 541 measureSafeArea={measureSafeArea} 542 imageAspect={imageAspect} 543 imageDimensions={dims ?? undefined} 544 dismissSwipePan={dismissSwipePan} 545 transforms={transforms} 546 /> 547 ) 548} 549 550function LightboxFooter({ 551 images, 552 index, 553 isAltExpanded, 554 toggleAltExpanded, 555 onPressSave, 556 onPressShare, 557}: { 558 images: ImageSource[] 559 index: number 560 isAltExpanded: boolean 561 toggleAltExpanded: () => void 562 onPressSave: (uri: string) => void 563 onPressShare: (uri: string) => void 564}) { 565 const {alt: altText, uri} = images[index] 566 const isMomentumScrolling = React.useRef(false) 567 return ( 568 <ScrollView 569 style={styles.footerScrollView} 570 scrollEnabled={isAltExpanded} 571 onMomentumScrollBegin={() => { 572 isMomentumScrolling.current = true 573 }} 574 onMomentumScrollEnd={() => { 575 isMomentumScrolling.current = false 576 }} 577 contentContainerStyle={{ 578 paddingVertical: 12, 579 paddingHorizontal: 24, 580 }}> 581 <SafeAreaView edges={['bottom']}> 582 {altText ? ( 583 <View accessibilityRole="button" style={styles.footerText}> 584 <Text 585 style={[s.gray3]} 586 numberOfLines={isAltExpanded ? undefined : 3} 587 selectable 588 onPress={() => { 589 if (isMomentumScrolling.current) { 590 return 591 } 592 LayoutAnimation.configureNext({ 593 duration: 450, 594 update: {type: 'spring', springDamping: 1}, 595 }) 596 toggleAltExpanded() 597 }} 598 onLongPress={() => {}}> 599 {altText} 600 </Text> 601 </View> 602 ) : null} 603 <View style={styles.footerBtns}> 604 <Button 605 type="primary-outline" 606 style={styles.footerBtn} 607 onPress={() => onPressSave(uri)}> 608 <FontAwesomeIcon icon={['far', 'floppy-disk']} style={s.white} /> 609 <Text type="xl" style={s.white}> 610 <Trans context="action">Save</Trans> 611 </Text> 612 </Button> 613 <Button 614 type="primary-outline" 615 style={styles.footerBtn} 616 onPress={() => onPressShare(uri)}> 617 <FontAwesomeIcon icon="arrow-up-from-bracket" style={s.white} /> 618 <Text type="xl" style={s.white}> 619 <Trans context="action">Share</Trans> 620 </Text> 621 </Button> 622 </View> 623 </SafeAreaView> 624 </ScrollView> 625 ) 626} 627 628const styles = StyleSheet.create({ 629 screen: { 630 position: 'absolute', 631 top: 0, 632 left: 0, 633 bottom: 0, 634 right: 0, 635 }, 636 screenHidden: { 637 opacity: 0, 638 pointerEvents: 'none', 639 }, 640 container: { 641 flex: 1, 642 }, 643 backdrop: { 644 backgroundColor: '#000', 645 position: 'absolute', 646 top: 0, 647 bottom: 0, 648 left: 0, 649 right: 0, 650 }, 651 controls: { 652 position: 'absolute', 653 top: 0, 654 bottom: 0, 655 left: 0, 656 right: 0, 657 gap: 20, 658 zIndex: 1, 659 pointerEvents: 'box-none', 660 }, 661 pager: { 662 flex: 1, 663 }, 664 header: { 665 position: 'absolute', 666 width: '100%', 667 top: 0, 668 pointerEvents: 'box-none', 669 }, 670 footer: { 671 position: 'absolute', 672 width: '100%', 673 maxHeight: '100%', 674 bottom: 0, 675 }, 676 footerScrollView: { 677 backgroundColor: '#000d', 678 flex: 1, 679 position: 'absolute', 680 bottom: 0, 681 width: '100%', 682 maxHeight: '100%', 683 }, 684 footerText: { 685 paddingBottom: isIOS ? 20 : 16, 686 }, 687 footerBtns: { 688 flexDirection: 'row', 689 justifyContent: 'center', 690 gap: 8, 691 }, 692 footerBtn: { 693 flexDirection: 'row', 694 alignItems: 'center', 695 gap: 8, 696 backgroundColor: 'transparent', 697 borderColor: colors.white, 698 }, 699}) 700 701function interpolatePx( 702 px: number, 703 inputRange: readonly number[], 704 outputRange: readonly number[], 705) { 706 'worklet' 707 const value = interpolate(px, inputRange, outputRange) 708 return Math.round(value * PIXEL_RATIO) / PIXEL_RATIO 709} 710 711function interpolateTransform( 712 progress: number, 713 thumbnailDims: { 714 pageX: number 715 width: number 716 pageY: number 717 height: number 718 }, 719 safeArea: {width: number; height: number; x: number; y: number}, 720 imageAspect: number, 721): { 722 scaleAndMoveTransform: Transform 723 cropFrameTransform: Transform 724 cropContentTransform: Transform 725 isResting: boolean 726 isHidden: boolean 727} { 728 'worklet' 729 const thumbAspect = thumbnailDims.width / thumbnailDims.height 730 let uncroppedInitialWidth 731 let uncroppedInitialHeight 732 if (imageAspect > thumbAspect) { 733 uncroppedInitialWidth = thumbnailDims.height * imageAspect 734 uncroppedInitialHeight = thumbnailDims.height 735 } else { 736 uncroppedInitialWidth = thumbnailDims.width 737 uncroppedInitialHeight = thumbnailDims.width / imageAspect 738 } 739 const safeAreaAspect = safeArea.width / safeArea.height 740 let finalWidth 741 let finalHeight 742 if (safeAreaAspect > imageAspect) { 743 finalWidth = safeArea.height * imageAspect 744 finalHeight = safeArea.height 745 } else { 746 finalWidth = safeArea.width 747 finalHeight = safeArea.width / imageAspect 748 } 749 const initialScale = Math.min( 750 uncroppedInitialWidth / finalWidth, 751 uncroppedInitialHeight / finalHeight, 752 ) 753 const croppedFinalWidth = thumbnailDims.width / initialScale 754 const croppedFinalHeight = thumbnailDims.height / initialScale 755 const screenCenterX = safeArea.width / 2 756 const screenCenterY = safeArea.height / 2 757 const thumbnailSafeAreaX = thumbnailDims.pageX - safeArea.x 758 const thumbnailSafeAreaY = thumbnailDims.pageY - safeArea.y 759 const thumbnailCenterX = thumbnailSafeAreaX + thumbnailDims.width / 2 760 const thumbnailCenterY = thumbnailSafeAreaY + thumbnailDims.height / 2 761 const initialTranslateX = thumbnailCenterX - screenCenterX 762 const initialTranslateY = thumbnailCenterY - screenCenterY 763 const scale = interpolate(progress, [0, 1], [initialScale, 1]) 764 const translateX = interpolatePx(progress, [0, 1], [initialTranslateX, 0]) 765 const translateY = interpolatePx(progress, [0, 1], [initialTranslateY, 0]) 766 const cropScaleX = interpolate( 767 progress, 768 [0, 1], 769 [croppedFinalWidth / finalWidth, 1], 770 ) 771 const cropScaleY = interpolate( 772 progress, 773 [0, 1], 774 [croppedFinalHeight / finalHeight, 1], 775 ) 776 return { 777 isHidden: false, 778 isResting: progress === 1, 779 scaleAndMoveTransform: [{translateX}, {translateY}, {scale}], 780 cropFrameTransform: [{scaleX: cropScaleX}, {scaleY: cropScaleY}], 781 cropContentTransform: [{scaleX: 1 / cropScaleX}, {scaleY: 1 / cropScaleY}], 782 } 783} 784 785function withClampedSpring(value: any, config: WithSpringConfig) { 786 'worklet' 787 return withSpring(value, {...config, overshootClamping: true}) 788} 789 790// We have to do this because we can't trust RN's rAF to fire in order. 791// https://github.com/facebook/react-native/issues/48005 792let isFrameScheduled = false 793let pendingFrameCallbacks: Array<() => void> = [] 794function rAF_FIXED(callback: () => void) { 795 pendingFrameCallbacks.push(callback) 796 if (!isFrameScheduled) { 797 isFrameScheduled = true 798 requestAnimationFrame(() => { 799 const callbacks = pendingFrameCallbacks.slice() 800 isFrameScheduled = false 801 pendingFrameCallbacks = [] 802 let hasError = false 803 let error 804 for (let i = 0; i < callbacks.length; i++) { 805 try { 806 callbacks[i]() 807 } catch (e) { 808 hasError = true 809 error = e 810 } 811 } 812 if (hasError) { 813 throw error 814 } 815 }) 816 } 817}