| author | |
| committer | |
| log | 98469159dd9e162a9c2f5cc4bbb2fbe89b3c4f67 |
| tree | 2567eb7897ab547b71132c98e36e5929668fcc85 |
| parent | 8e129a0dfb35ac2c03a7320245b5bfb976f5d3bf |
| signature | Signed by PGP key B5690EEEBB952194 |
5 files changed, 339 insertions(+), 103 deletions(-)
apps/mobile/src/features/home/HomeScreen.tsx+1-6| ... | @@ -490,12 +490,7 @@ export function HomeScreen(props: HomeScreenProps) { | ... | @@ -490,12 +490,7 @@ export function HomeScreen(props: HomeScreenProps) { |
| 490 | // Settled threads stay in the live shell stream (settled ≠ archived), so | 490 | // Settled threads stay in the live shell stream (settled ≠ archived), so |
| 491 | // the partition works directly off live shells — no snapshot merging or | 491 | // the partition works directly off live shells — no snapshot merging or |
| 492 | // optimistic holds. | 492 | // optimistic holds. |
| 493 | const handleSettleThread = useCallback( | 493 | const handleSettleThread = props.onSettleThread; |
| 494 | (thread: EnvironmentThreadShell) => { | ||
| 495 | void props.onSettleThread(thread); | ||
| 496 | }, | ||
| 497 | [props.onSettleThread], | ||
| 498 | ); | ||
| 499 | const handleSnoozeThread = useCallback( | 494 | const handleSnoozeThread = useCallback( |
| 500 | (thread: EnvironmentThreadShell, snoozedUntil: string) => { | 495 | (thread: EnvironmentThreadShell, snoozedUntil: string) => { |
| 501 | void props.onSnoozeThread(thread, snoozedUntil); | 496 | void props.onSnoozeThread(thread, snoozedUntil); |
apps/mobile/src/features/home/thread-swipe-actions.tsx+223-80| ... | @@ -7,6 +7,7 @@ import { | ... | @@ -7,6 +7,7 @@ import { |
| 7 | use, | 7 | use, |
| 8 | useCallback, | 8 | useCallback, |
| 9 | useEffect, | 9 | useEffect, |
| 10 | useLayoutEffect, | ||
| 10 | useRef, | 11 | useRef, |
| 11 | useState, | 12 | useState, |
| 12 | type ComponentProps, | 13 | type ComponentProps, |
| ... | @@ -19,17 +20,23 @@ import type { | ... | @@ -19,17 +20,23 @@ import type { |
| 19 | StyleProp, | 20 | StyleProp, |
| 20 | ViewStyle, | 21 | ViewStyle, |
| 21 | } from "react-native"; | 22 | } from "react-native"; |
| 22 | import { Pressable, View } from "react-native"; | 23 | import { Alert, Pressable, View } from "react-native"; |
| 23 | import ReanimatedSwipeable, { | 24 | import ReanimatedSwipeable, { |
| 24 | type SwipeableMethods, | 25 | type SwipeableMethods, |
| 25 | } from "react-native-gesture-handler/ReanimatedSwipeable"; | 26 | } from "react-native-gesture-handler/ReanimatedSwipeable"; |
| 26 | import Animated, { | 27 | import Animated, { |
| 28 | cancelAnimation, | ||
| 29 | Easing, | ||
| 27 | Extrapolation, | 30 | Extrapolation, |
| 31 | ReduceMotion, | ||
| 28 | interpolate, | 32 | interpolate, |
| 29 | runOnJS, | 33 | runOnJS, |
| 34 | runOnUI, | ||
| 30 | type SharedValue, | 35 | type SharedValue, |
| 31 | useAnimatedReaction, | 36 | useAnimatedReaction, |
| 32 | useAnimatedStyle, | 37 | useAnimatedStyle, |
| 38 | useSharedValue, | ||
| 39 | withTiming, | ||
| 33 | } from "react-native-reanimated"; | 40 | } from "react-native-reanimated"; |
| 34 | 41 | ||
| 35 | import { AppText as Text } from "../../components/AppText"; | 42 | import { AppText as Text } from "../../components/AppText"; |
| ... | @@ -61,6 +68,13 @@ interface ThreadSwipeAction { | ... | @@ -61,6 +68,13 @@ interface ThreadSwipeAction { |
| 61 | readonly onPress: () => void; | 68 | readonly onPress: () => void; |
| 62 | } | 69 | } |
| 63 | 70 | ||
| 71 | /** Dismiss before committing; false restores the row, success changes its resetKey or removes it. */ | ||
| 72 | type ThreadSwipePrimaryAction = Omit<ThreadSwipeAction, "onPress"> & | ||
| 73 | ( | ||
| 74 | | { readonly dismissOnPress: true; readonly onPress: () => Promise<boolean> } | ||
| 75 | | { readonly dismissOnPress?: false; readonly onPress: () => void } | ||
| 76 | ); | ||
| 77 | |||
| 64 | interface ThreadSwipeSecondaryAction extends ThreadSwipeAction { | 78 | interface ThreadSwipeSecondaryAction extends ThreadSwipeAction { |
| 65 | readonly tone: "primary" | "secondary" | "danger"; | 79 | readonly tone: "primary" | "secondary" | "danger"; |
| 66 | } | 80 | } |
| ... | @@ -218,7 +232,7 @@ export function useSwipeableScrollGate(options?: { | ... | @@ -218,7 +232,7 @@ export function useSwipeableScrollGate(options?: { |
| 218 | }; | 232 | }; |
| 219 | } | 233 | } |
| 220 | 234 | ||
| 221 | export function ThreadSwipeable(props: { | 235 | interface ThreadSwipeableProps { |
| 222 | readonly backgroundColor: ColorValue; | 236 | readonly backgroundColor: ColorValue; |
| 223 | readonly children: (close: () => void) => ReactNode; | 237 | readonly children: (close: () => void) => ReactNode; |
| 224 | /** Uses action visuals that fit inside compact 44pt rows. The press target | 238 | /** Uses action visuals that fit inside compact 44pt rows. The press target |
| ... | @@ -238,7 +252,7 @@ export function ThreadSwipeable(props: { | ... | @@ -238,7 +252,7 @@ export function ThreadSwipeable(props: { |
| 238 | readonly onDelete: () => void; | 252 | readonly onDelete: () => void; |
| 239 | readonly onSwipeableClose?: (methods: SwipeableMethods) => void; | 253 | readonly onSwipeableClose?: (methods: SwipeableMethods) => void; |
| 240 | readonly onSwipeableWillOpen?: (methods: SwipeableMethods) => void; | 254 | readonly onSwipeableWillOpen?: (methods: SwipeableMethods) => void; |
| 241 | readonly primaryAction: ThreadSwipeAction; | 255 | readonly primaryAction: ThreadSwipePrimaryAction; |
| 242 | /** | 256 | /** |
| 243 | * Omitted keeps the v1 destructive Delete action. Explicit null opts out of | 257 | * Omitted keeps the v1 destructive Delete action. Explicit null opts out of |
| 244 | * a secondary action entirely so a gated Snooze can never fall back to an | 258 | * a secondary action entirely so a gated Snooze can never fall back to an |
| ... | @@ -255,7 +269,15 @@ export function ThreadSwipeable(props: { | ... | @@ -255,7 +269,15 @@ export function ThreadSwipeable(props: { |
| 255 | typeof ReanimatedSwipeable | 269 | typeof ReanimatedSwipeable |
| 256 | >["simultaneousWithExternalGesture"]; | 270 | >["simultaneousWithExternalGesture"]; |
| 257 | readonly threadTitle: string; | 271 | readonly threadTitle: string; |
| 258 | }) { | 272 | } |
| 273 | |||
| 274 | export function ThreadSwipeable(props: ThreadSwipeableProps) { | ||
| 275 | // Recycled content gets fresh native and animation state. Late callbacks | ||
| 276 | // from the previous row retain its action, never the replacement's action. | ||
| 277 | return <ThreadSwipeableRow key={props.resetKey} {...props} />; | ||
| 278 | } | ||
| 279 | |||
| 280 | function ThreadSwipeableRow(props: ThreadSwipeableProps) { | ||
| 259 | const swipeableRef = useRef<SwipeableMethods | null>(null); | 281 | const swipeableRef = useRef<SwipeableMethods | null>(null); |
| 260 | const fullSwipeArmedRef = useRef(false); | 282 | const fullSwipeArmedRef = useRef(false); |
| 261 | const hasSecondaryAction = props.secondaryAction !== null; | 283 | const hasSecondaryAction = props.secondaryAction !== null; |
| ... | @@ -265,14 +287,119 @@ export function ThreadSwipeable(props: { | ... | @@ -265,14 +287,119 @@ export function ThreadSwipeable(props: { |
| 265 | props.fullSwipeAction ?? (props.secondaryAction === undefined ? "delete" : "primary"); | 287 | props.fullSwipeAction ?? (props.secondaryAction === undefined ? "delete" : "primary"); |
| 266 | const close = useCallback(() => swipeableRef.current?.close(), []); | 288 | const close = useCallback(() => swipeableRef.current?.close(), []); |
| 267 | const gateEnabled = use(SwipeableScrollGateContext); | 289 | const gateEnabled = use(SwipeableScrollGateContext); |
| 268 | const resetKey = props.resetKey; | 290 | const mountedRef = useRef(true); |
| 269 | useEffect(() => { | 291 | const pendingDismissRef = useRef<(() => Promise<boolean>) | null>(null); |
| 270 | if (resetKey === undefined) { | 292 | const activeTranslationRef = useRef<SharedValue<number> | null>(null); |
| 271 | return; | 293 | const [isDismissing, setIsDismissing] = useState(false); |
| 294 | const dismissing = useSharedValue(false); | ||
| 295 | const rowHeight = useSharedValue(0); | ||
| 296 | const rowWidth = useSharedValue(props.fullSwipeWidth); | ||
| 297 | const collapse = useSharedValue(0); | ||
| 298 | const actionOpacity = useSharedValue(1); | ||
| 299 | const primaryAction = props.primaryAction; | ||
| 300 | const onSwipeableClose = props.onSwipeableClose; | ||
| 301 | |||
| 302 | const restoreRow = useCallback(() => { | ||
| 303 | swipeableRef.current?.close(); | ||
| 304 | collapse.set(0); | ||
| 305 | actionOpacity.set(1); | ||
| 306 | dismissing.set(false); | ||
| 307 | setIsDismissing(false); | ||
| 308 | }, [actionOpacity, collapse, dismissing]); | ||
| 309 | |||
| 310 | const finishDismiss = useCallback(async () => { | ||
| 311 | const action = pendingDismissRef.current; | ||
| 312 | if (!action) return; | ||
| 313 | pendingDismissRef.current = null; | ||
| 314 | try { | ||
| 315 | const succeeded = await action(); | ||
| 316 | if (!succeeded && mountedRef.current) restoreRow(); | ||
| 317 | } catch (error) { | ||
| 318 | if (mountedRef.current) restoreRow(); | ||
| 319 | Alert.alert( | ||
| 320 | "Could not settle thread", | ||
| 321 | error instanceof Error ? error.message : "The thread could not be settled.", | ||
| 322 | ); | ||
| 272 | } | 323 | } |
| 273 | fullSwipeArmedRef.current = false; | 324 | }, [restoreRow]); |
| 274 | swipeableRef.current?.reset(); | 325 | |
| 275 | }, [resetKey]); | 326 | useLayoutEffect(() => { |
| 327 | mountedRef.current = true; | ||
| 328 | return () => { | ||
| 329 | mountedRef.current = false; | ||
| 330 | cancelAnimation(collapse); | ||
| 331 | cancelAnimation(actionOpacity); | ||
| 332 | if (activeTranslationRef.current) cancelAnimation(activeTranslationRef.current); | ||
| 333 | // Scrolling a committed row out of the recycled list must still settle it. | ||
| 334 | void finishDismiss(); | ||
| 335 | }; | ||
| 336 | }, [actionOpacity, collapse, finishDismiss]); | ||
| 337 | |||
| 338 | const beginDismiss = useCallback( | ||
| 339 | (translation: SharedValue<number>) => { | ||
| 340 | if (!primaryAction.dismissOnPress) return; | ||
| 341 | pendingDismissRef.current = primaryAction.onPress; | ||
| 342 | activeTranslationRef.current = translation; | ||
| 343 | fullSwipeArmedRef.current = false; | ||
| 344 | if (!mountedRef.current) { | ||
| 345 | void finishDismiss(); | ||
| 346 | return; | ||
| 347 | } | ||
| 348 | setIsDismissing(true); | ||
| 349 | if (swipeableRef.current) onSwipeableClose?.(swipeableRef.current); | ||
| 350 | }, | ||
| 351 | [finishDismiss, primaryAction, onSwipeableClose], | ||
| 352 | ); | ||
| 353 | |||
| 354 | const dismiss = useCallback( | ||
| 355 | (translation: SharedValue<number>) => { | ||
| 356 | "worklet"; | ||
| 357 | if (dismissing.value) return; | ||
| 358 | dismissing.set(true); | ||
| 359 | runOnJS(beginDismiss)(translation); | ||
| 360 | const timing = { | ||
| 361 | duration: 220, | ||
| 362 | easing: Easing.out(Easing.cubic), | ||
| 363 | reduceMotion: ReduceMotion.System, | ||
| 364 | }; | ||
| 365 | actionOpacity.set(withTiming(0, timing)); | ||
| 366 | // Never reverse a swipe that already carried the row beyond its width. | ||
| 367 | translation.set( | ||
| 368 | withTiming(Math.min(translation.value, -rowWidth.value), timing, (finished) => { | ||
| 369 | if (!finished) return; | ||
| 370 | collapse.set( | ||
| 371 | withTiming(1, { ...timing, duration: 180 }, (collapsed) => { | ||
| 372 | if (collapsed) runOnJS(finishDismiss)(); | ||
| 373 | }), | ||
| 374 | ); | ||
| 375 | }), | ||
| 376 | ); | ||
| 377 | }, | ||
| 378 | [actionOpacity, beginDismiss, collapse, dismissing, finishDismiss, rowWidth], | ||
| 379 | ); | ||
| 380 | const dismissStyle = useAnimatedStyle(() => ({ | ||
| 381 | height: dismissing.value ? rowHeight.value * (1 - collapse.value) : undefined, | ||
| 382 | pointerEvents: dismissing.value ? "none" : "auto", | ||
| 383 | overflow: "hidden", | ||
| 384 | })); | ||
| 385 | const actionStyle = useAnimatedStyle(() => ({ opacity: actionOpacity.value, height: "100%" })); | ||
| 386 | const dismissOnPress = primaryAction.dismissOnPress === true; | ||
| 387 | const handleRelease = useCallback( | ||
| 388 | (translation: SharedValue<number>) => { | ||
| 389 | "worklet"; | ||
| 390 | if (dismissing.value) return true; | ||
| 391 | if ( | ||
| 392 | dismissOnPress && | ||
| 393 | fullSwipeAction === "primary" && | ||
| 394 | -translation.value >= fullSwipeThreshold | ||
| 395 | ) { | ||
| 396 | dismiss(translation); | ||
| 397 | return true; | ||
| 398 | } | ||
| 399 | return false; | ||
| 400 | }, | ||
| 401 | [dismiss, dismissing, dismissOnPress, fullSwipeAction, fullSwipeThreshold], | ||
| 402 | ); | ||
| 276 | const handleFullSwipeArmedChange = useCallback((armed: boolean) => { | 403 | const handleFullSwipeArmedChange = useCallback((armed: boolean) => { |
| 277 | if (armed && !fullSwipeArmedRef.current) { | 404 | if (armed && !fullSwipeArmedRef.current) { |
| 278 | void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); | 405 | void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); |
| ... | @@ -281,78 +408,94 @@ export function ThreadSwipeable(props: { | ... | @@ -281,78 +408,94 @@ export function ThreadSwipeable(props: { |
| 281 | }, []); | 408 | }, []); |
| 282 | 409 | ||
| 283 | return ( | 410 | return ( |
| 284 | <ReanimatedSwipeable | 411 | <Animated.View style={dismissStyle}> |
| 285 | ref={swipeableRef} | 412 | <View |
| 286 | animationOptions={THREAD_SWIPE_SPRING} | 413 | onLayout={({ nativeEvent: { layout } }) => { |
| 287 | childrenContainerStyle={{ backgroundColor: props.backgroundColor }} | 414 | rowHeight.set(layout.height); |
| 288 | containerStyle={[{ backgroundColor: props.backgroundColor }, props.containerStyle]} | 415 | rowWidth.set(layout.width); |
| 289 | dragOffsetFromRightEdge={8} | 416 | }} |
| 290 | enabled={props.enabled !== false && gateEnabled} | 417 | > |
| 291 | enableTrackpadTwoFingerGesture={props.enableTrackpadSwipe ?? true} | 418 | <ReanimatedSwipeable |
| 292 | // Fail the swipe once the pan is vertically dominant (patched-in RNGH | 419 | ref={swipeableRef} |
| 293 | // prop) — otherwise trackpad scrolls with ~8px of horizontal drift | 420 | animationOptions={THREAD_SWIPE_SPRING} |
| 294 | // start opening rows because the swipe pan runs simultaneously with | 421 | childrenContainerStyle={{ backgroundColor: props.backgroundColor }} |
| 295 | // the list scroll gesture and never gets disqualified by Y movement. | 422 | containerStyle={[{ backgroundColor: props.backgroundColor }, props.containerStyle]} |
| 296 | failOffsetY={[-10, 10]} | 423 | dragOffsetFromRightEdge={8} |
| 297 | friction={1} | 424 | enabled={!isDismissing && props.enabled !== false && gateEnabled} |
| 298 | onSwipeableClose={() => { | 425 | enableTrackpadTwoFingerGesture={props.enableTrackpadSwipe ?? true} |
| 299 | fullSwipeArmedRef.current = false; | 426 | // Fail the swipe once the pan is vertically dominant (patched-in RNGH |
| 300 | if (swipeableRef.current) { | 427 | // prop) — otherwise trackpad scrolls with ~8px of horizontal drift |
| 301 | props.onSwipeableClose?.(swipeableRef.current); | 428 | // start opening rows because the swipe pan runs simultaneously with |
| 302 | } | 429 | // the list scroll gesture and never gets disqualified by Y movement. |
| 303 | }} | 430 | failOffsetY={[-10, 10]} |
| 304 | onSwipeableOpenStartDrag={() => { | 431 | friction={1} |
| 305 | if (swipeableRef.current) { | 432 | onSwipeableClose={() => { |
| 306 | props.onSwipeableWillOpen?.(swipeableRef.current); | 433 | fullSwipeArmedRef.current = false; |
| 307 | } | 434 | if (swipeableRef.current) { |
| 308 | }} | 435 | props.onSwipeableClose?.(swipeableRef.current); |
| 309 | onSwipeableWillOpen={() => { | 436 | } |
| 310 | const methods = swipeableRef.current; | 437 | }} |
| 311 | if (!methods) { | 438 | onSwipeableRelease={handleRelease} |
| 312 | return; | 439 | onSwipeableOpenStartDrag={() => { |
| 313 | } | 440 | if (swipeableRef.current) { |
| 441 | props.onSwipeableWillOpen?.(swipeableRef.current); | ||
| 442 | } | ||
| 443 | }} | ||
| 444 | onSwipeableWillOpen={() => { | ||
| 445 | const methods = swipeableRef.current; | ||
| 446 | if (!methods) { | ||
| 447 | return; | ||
| 448 | } | ||
| 314 | 449 | ||
| 315 | props.onSwipeableWillOpen?.(methods); | 450 | props.onSwipeableWillOpen?.(methods); |
| 316 | if (fullSwipeArmedRef.current) { | 451 | if (fullSwipeArmedRef.current && !(dismissOnPress && fullSwipeAction === "primary")) { |
| 317 | fullSwipeArmedRef.current = false; | 452 | fullSwipeArmedRef.current = false; |
| 318 | methods.close(); | ||
| 319 | if (fullSwipeAction === "primary") { | ||
| 320 | props.primaryAction.onPress(); | ||
| 321 | } else { | ||
| 322 | props.onDelete(); | ||
| 323 | } | ||
| 324 | } | ||
| 325 | }} | ||
| 326 | overshootFriction={1} | ||
| 327 | overshootRight | ||
| 328 | renderRightActions={(_progress, translation, methods) => ( | ||
| 329 | <ThreadSwipeActions | ||
| 330 | backgroundColor={props.backgroundColor} | ||
| 331 | compact={props.compactActions === true} | ||
| 332 | fullSwipeAction={fullSwipeAction} | ||
| 333 | fullSwipeThreshold={fullSwipeThreshold} | ||
| 334 | onFullSwipeArmedChange={handleFullSwipeArmedChange} | ||
| 335 | primaryAction={{ | ||
| 336 | ...props.primaryAction, | ||
| 337 | onPress: () => { | ||
| 338 | methods.close(); | 453 | methods.close(); |
| 339 | props.primaryAction.onPress(); | 454 | if (fullSwipeAction === "primary") { |
| 340 | }, | 455 | props.primaryAction.onPress(); |
| 456 | } else { | ||
| 457 | props.onDelete(); | ||
| 458 | } | ||
| 459 | } | ||
| 341 | }} | 460 | }} |
| 342 | secondaryAction={resolveSecondaryAction({ | 461 | overshootFriction={1} |
| 343 | close: () => methods.close(), | 462 | overshootRight |
| 344 | onDelete: props.onDelete, | 463 | renderRightActions={(_progress, translation, methods) => ( |
| 345 | secondaryAction: props.secondaryAction, | 464 | <Animated.View style={actionStyle}> |
| 346 | threadTitle: props.threadTitle, | 465 | <ThreadSwipeActions |
| 347 | })} | 466 | backgroundColor={props.backgroundColor} |
| 348 | translation={translation} | 467 | compact={props.compactActions === true} |
| 349 | /> | 468 | fullSwipeAction={fullSwipeAction} |
| 350 | )} | 469 | fullSwipeThreshold={fullSwipeThreshold} |
| 351 | rightThreshold={actionsWidth * 0.42} | 470 | onFullSwipeArmedChange={handleFullSwipeArmedChange} |
| 352 | simultaneousWithExternalGesture={props.simultaneousWithExternalGesture} | 471 | primaryAction={{ |
| 353 | > | 472 | ...primaryAction, |
| 354 | {props.children(close)} | 473 | onPress: () => { |
| 355 | </ReanimatedSwipeable> | 474 | if (primaryAction.dismissOnPress) { |
| 475 | runOnUI(dismiss)(translation); | ||
| 476 | } else { | ||
| 477 | methods.close(); | ||
| 478 | primaryAction.onPress(); | ||
| 479 | } | ||
| 480 | }, | ||
| 481 | }} | ||
| 482 | secondaryAction={resolveSecondaryAction({ | ||
| 483 | close: () => methods.close(), | ||
| 484 | onDelete: props.onDelete, | ||
| 485 | secondaryAction: props.secondaryAction, | ||
| 486 | threadTitle: props.threadTitle, | ||
| 487 | })} | ||
| 488 | translation={translation} | ||
| 489 | /> | ||
| 490 | </Animated.View> | ||
| 491 | )} | ||
| 492 | rightThreshold={actionsWidth * 0.42} | ||
| 493 | simultaneousWithExternalGesture={props.simultaneousWithExternalGesture} | ||
| 494 | > | ||
| 495 | {props.children(close)} | ||
| 496 | </ReanimatedSwipeable> | ||
| 497 | </View> | ||
| 498 | </Animated.View> | ||
| 356 | ); | 499 | ); |
| 357 | } | 500 | } |
| 358 | 501 |
apps/mobile/src/features/threads/thread-list-v2-items.tsx+3-2| ... | @@ -371,7 +371,7 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { | ... | @@ -371,7 +371,7 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { |
| 371 | readonly onSelectThread: (thread: EnvironmentThreadShell) => void; | 371 | readonly onSelectThread: (thread: EnvironmentThreadShell) => void; |
| 372 | readonly onDeleteThread: (thread: EnvironmentThreadShell) => void; | 372 | readonly onDeleteThread: (thread: EnvironmentThreadShell) => void; |
| 373 | readonly onRegenerateThreadTitle: (thread: EnvironmentThreadShell) => void; | 373 | readonly onRegenerateThreadTitle: (thread: EnvironmentThreadShell) => void; |
| 374 | readonly onSettleThread: (thread: EnvironmentThreadShell) => void; | 374 | readonly onSettleThread: (thread: EnvironmentThreadShell) => Promise<boolean>; |
| 375 | readonly onSnoozeThread: (thread: EnvironmentThreadShell, snoozedUntil: string) => void; | 375 | readonly onSnoozeThread: (thread: EnvironmentThreadShell, snoozedUntil: string) => void; |
| 376 | readonly onUnsnoozeThread: (thread: EnvironmentThreadShell) => void; | 376 | readonly onUnsnoozeThread: (thread: EnvironmentThreadShell) => void; |
| 377 | readonly onUnsettleThread: (thread: EnvironmentThreadShell) => void; | 377 | readonly onUnsettleThread: (thread: EnvironmentThreadShell) => void; |
| ... | @@ -653,6 +653,7 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { | ... | @@ -653,6 +653,7 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { |
| 653 | accessibilityLabel: `Settle ${thread.title}`, | 653 | accessibilityLabel: `Settle ${thread.title}`, |
| 654 | icon: "checkmark" as const, | 654 | icon: "checkmark" as const, |
| 655 | label: "Settle", | 655 | label: "Settle", |
| 656 | dismissOnPress: true as const, | ||
| 656 | onPress: handleSettle, | 657 | onPress: handleSettle, |
| 657 | }; | 658 | }; |
| 658 | }, [ | 659 | }, [ |
| ... | @@ -963,7 +964,7 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { | ... | @@ -963,7 +964,7 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { |
| 963 | onSwipeableWillOpen={props.onSwipeableWillOpen} | 964 | onSwipeableWillOpen={props.onSwipeableWillOpen} |
| 964 | primaryAction={primaryAction} | 965 | primaryAction={primaryAction} |
| 965 | secondaryAction={secondaryAction} | 966 | secondaryAction={secondaryAction} |
| 966 | resetKey={`${thread.environmentId}:${thread.id}`} | 967 | resetKey={`${thread.environmentId}:${thread.id}:${variant}:${snoozedRow}`} |
| 967 | simultaneousWithExternalGesture={props.simultaneousSwipeGesture} | 968 | simultaneousWithExternalGesture={props.simultaneousSwipeGesture} |
| 968 | threadTitle={thread.title} | 969 | threadTitle={thread.title} |
| 969 | > | 970 | > |
patches/react-native-gesture-handler@2.32.0.patch+109-12| ... | @@ -1,8 +1,8 @@ | ... | @@ -1,8 +1,8 @@ |
| 1 | diff --git a/lib/commonjs/components/ReanimatedSwipeable/ReanimatedSwipeable.js b/lib/commonjs/components/ReanimatedSwipeable/ReanimatedSwipeable.js | 1 | diff --git a/lib/commonjs/components/ReanimatedSwipeable/ReanimatedSwipeable.js b/lib/commonjs/components/ReanimatedSwipeable/ReanimatedSwipeable.js |
| 2 | index 551ab92bf58db0b79d428dcd2f2df898ef686493..f1c355b8e40cd4f583ef3a501b044fb6770f4a24 100644 | 2 | index 551ab92bf58db0b79d428dcd2f2df898ef686493..13db26d88bff975bc5d46bb6432cbf9fda3d489a 100644 |
| 3 | --- a/lib/commonjs/components/ReanimatedSwipeable/ReanimatedSwipeable.js | 3 | --- a/lib/commonjs/components/ReanimatedSwipeable/ReanimatedSwipeable.js |
| 4 | +++ b/lib/commonjs/components/ReanimatedSwipeable/ReanimatedSwipeable.js | 4 | +++ b/lib/commonjs/components/ReanimatedSwipeable/ReanimatedSwipeable.js |
| 5 | @@ -34,6 +34,7 @@ const Swipeable = props => { | 5 | @@ -34,11 +34,13 @@ const Swipeable = props => { |
| 6 | enableTrackpadTwoFingerGesture = DEFAULT_ENABLE_TRACKING_TWO_FINGER_GESTURE, | 6 | enableTrackpadTwoFingerGesture = DEFAULT_ENABLE_TRACKING_TWO_FINGER_GESTURE, |
| 7 | dragOffsetFromLeftEdge = DEFAULT_DRAG_OFFSET, | 7 | dragOffsetFromLeftEdge = DEFAULT_DRAG_OFFSET, |
| 8 | dragOffsetFromRightEdge = DEFAULT_DRAG_OFFSET, | 8 | dragOffsetFromRightEdge = DEFAULT_DRAG_OFFSET, |
| ... | @@ -10,7 +10,28 @@ index 551ab92bf58db0b79d428dcd2f2df898ef686493..f1c355b8e40cd4f583ef3a501b044fb6 | ... | @@ -10,7 +10,28 @@ index 551ab92bf58db0b79d428dcd2f2df898ef686493..f1c355b8e40cd4f583ef3a501b044fb6 |
| 10 | friction = DEFAULT_FRICTION, | 10 | friction = DEFAULT_FRICTION, |
| 11 | overshootFriction = DEFAULT_OVERSHOOT_FRICTION, | 11 | overshootFriction = DEFAULT_OVERSHOOT_FRICTION, |
| 12 | onSwipeableOpenStartDrag, | 12 | onSwipeableOpenStartDrag, |
| 13 | @@ -285,11 +286,14 @@ const Swipeable = props => { | 13 | onSwipeableCloseStartDrag, |
| 14 | onSwipeableWillOpen, | ||
| 15 | + onSwipeableRelease, | ||
| 16 | onSwipeableWillClose, | ||
| 17 | onSwipeableOpen, | ||
| 18 | onSwipeableClose, | ||
| 19 | @@ -248,8 +250,13 @@ const Swipeable = props => { | ||
| 20 | toValue = -rightWidth.value; | ||
| 21 | } | ||
| 22 | } | ||
| 23 | + // Let a UI-thread full-swipe commit take over before the return spring. | ||
| 24 | + if (onSwipeableRelease?.(appliedTranslation)) { | ||
| 25 | + return; | ||
| 26 | + } | ||
| 27 | + | ||
| 28 | animateRow(toValue, velocityX / friction); | ||
| 29 | - }, [animateRow, friction, leftThreshold, leftWidth, rightThreshold, rightWidth, rowState, userDrag]); | ||
| 30 | + }, [animateRow, friction, leftThreshold, leftWidth, rightThreshold, rightWidth, rowState, userDrag, onSwipeableRelease, appliedTranslation]); | ||
| 31 | const close = (0, _react.useCallback)(() => { | ||
| 32 | 'worklet'; | ||
| 33 | |||
| 34 | @@ -285,11 +292,14 @@ const Swipeable = props => { | ||
| 14 | }).onFinalize(() => { | 35 | }).onFinalize(() => { |
| 15 | dragStarted.value = false; | 36 | dragStarted.value = false; |
| 16 | }); | 37 | }); |
| ... | @@ -27,10 +48,10 @@ index 551ab92bf58db0b79d428dcd2f2df898ef686493..f1c355b8e40cd4f583ef3a501b044fb6 | ... | @@ -27,10 +48,10 @@ index 551ab92bf58db0b79d428dcd2f2df898ef686493..f1c355b8e40cd4f583ef3a501b044fb6 |
| 27 | const animatedStyle = (0, _reactNativeReanimated.useAnimatedStyle)(() => ({ | 48 | const animatedStyle = (0, _reactNativeReanimated.useAnimatedStyle)(() => ({ |
| 28 | transform: [{ | 49 | transform: [{ |
| 29 | diff --git a/lib/module/components/ReanimatedSwipeable/ReanimatedSwipeable.js b/lib/module/components/ReanimatedSwipeable/ReanimatedSwipeable.js | 50 | diff --git a/lib/module/components/ReanimatedSwipeable/ReanimatedSwipeable.js b/lib/module/components/ReanimatedSwipeable/ReanimatedSwipeable.js |
| 30 | index a2835d5416ffd5cf9a04e98774516b9e6569691e..b73c177227bf3a232737b0eb1c0e2bb830493c22 100644 | 51 | index a2835d5416ffd5cf9a04e98774516b9e6569691e..055afce410d6eb655532e7a09371dfc22eae5e9c 100644 |
| 31 | --- a/lib/module/components/ReanimatedSwipeable/ReanimatedSwipeable.js | 52 | --- a/lib/module/components/ReanimatedSwipeable/ReanimatedSwipeable.js |
| 32 | +++ b/lib/module/components/ReanimatedSwipeable/ReanimatedSwipeable.js | 53 | +++ b/lib/module/components/ReanimatedSwipeable/ReanimatedSwipeable.js |
| 33 | @@ -29,6 +29,7 @@ const Swipeable = props => { | 54 | @@ -29,11 +29,13 @@ const Swipeable = props => { |
| 34 | enableTrackpadTwoFingerGesture = DEFAULT_ENABLE_TRACKING_TWO_FINGER_GESTURE, | 55 | enableTrackpadTwoFingerGesture = DEFAULT_ENABLE_TRACKING_TWO_FINGER_GESTURE, |
| 35 | dragOffsetFromLeftEdge = DEFAULT_DRAG_OFFSET, | 56 | dragOffsetFromLeftEdge = DEFAULT_DRAG_OFFSET, |
| 36 | dragOffsetFromRightEdge = DEFAULT_DRAG_OFFSET, | 57 | dragOffsetFromRightEdge = DEFAULT_DRAG_OFFSET, |
| ... | @@ -38,7 +59,28 @@ index a2835d5416ffd5cf9a04e98774516b9e6569691e..b73c177227bf3a232737b0eb1c0e2bb8 | ... | @@ -38,7 +59,28 @@ index a2835d5416ffd5cf9a04e98774516b9e6569691e..b73c177227bf3a232737b0eb1c0e2bb8 |
| 38 | friction = DEFAULT_FRICTION, | 59 | friction = DEFAULT_FRICTION, |
| 39 | overshootFriction = DEFAULT_OVERSHOOT_FRICTION, | 60 | overshootFriction = DEFAULT_OVERSHOOT_FRICTION, |
| 40 | onSwipeableOpenStartDrag, | 61 | onSwipeableOpenStartDrag, |
| 41 | @@ -280,11 +281,14 @@ const Swipeable = props => { | 62 | onSwipeableCloseStartDrag, |
| 63 | onSwipeableWillOpen, | ||
| 64 | + onSwipeableRelease, | ||
| 65 | onSwipeableWillClose, | ||
| 66 | onSwipeableOpen, | ||
| 67 | onSwipeableClose, | ||
| 68 | @@ -243,8 +245,13 @@ const Swipeable = props => { | ||
| 69 | toValue = -rightWidth.value; | ||
| 70 | } | ||
| 71 | } | ||
| 72 | + // Let a UI-thread full-swipe commit take over before the return spring. | ||
| 73 | + if (onSwipeableRelease?.(appliedTranslation)) { | ||
| 74 | + return; | ||
| 75 | + } | ||
| 76 | + | ||
| 77 | animateRow(toValue, velocityX / friction); | ||
| 78 | - }, [animateRow, friction, leftThreshold, leftWidth, rightThreshold, rightWidth, rowState, userDrag]); | ||
| 79 | + }, [animateRow, friction, leftThreshold, leftWidth, rightThreshold, rightWidth, rowState, userDrag, onSwipeableRelease, appliedTranslation]); | ||
| 80 | const close = useCallback(() => { | ||
| 81 | 'worklet'; | ||
| 82 | |||
| 83 | @@ -280,11 +287,14 @@ const Swipeable = props => { | ||
| 42 | }).onFinalize(() => { | 84 | }).onFinalize(() => { |
| 43 | dragStarted.value = false; | 85 | dragStarted.value = false; |
| 44 | }); | 86 | }); |
| ... | @@ -55,7 +97,7 @@ index a2835d5416ffd5cf9a04e98774516b9e6569691e..b73c177227bf3a232737b0eb1c0e2bb8 | ... | @@ -55,7 +97,7 @@ index a2835d5416ffd5cf9a04e98774516b9e6569691e..b73c177227bf3a232737b0eb1c0e2bb8 |
| 55 | const animatedStyle = useAnimatedStyle(() => ({ | 97 | const animatedStyle = useAnimatedStyle(() => ({ |
| 56 | transform: [{ | 98 | transform: [{ |
| 57 | diff --git a/lib/typescript/components/ReanimatedSwipeable/ReanimatedSwipeableProps.d.ts b/lib/typescript/components/ReanimatedSwipeable/ReanimatedSwipeableProps.d.ts | 99 | diff --git a/lib/typescript/components/ReanimatedSwipeable/ReanimatedSwipeableProps.d.ts b/lib/typescript/components/ReanimatedSwipeable/ReanimatedSwipeableProps.d.ts |
| 58 | index ac8b76830d468edfbc29052b452a36221323c3de..589e329d2aa706ddfff0d1037df0d85a050edbb0 100644 | 100 | index ac8b76830d468edfbc29052b452a36221323c3de..6985d54d9d359e825a5a0e6078bb113204e2807b 100644 |
| 59 | --- a/lib/typescript/components/ReanimatedSwipeable/ReanimatedSwipeableProps.d.ts | 101 | --- a/lib/typescript/components/ReanimatedSwipeable/ReanimatedSwipeableProps.d.ts |
| 60 | +++ b/lib/typescript/components/ReanimatedSwipeable/ReanimatedSwipeableProps.d.ts | 102 | +++ b/lib/typescript/components/ReanimatedSwipeable/ReanimatedSwipeableProps.d.ts |
| 61 | @@ -64,6 +64,13 @@ export interface SwipeableProps { | 103 | @@ -64,6 +64,13 @@ export interface SwipeableProps { |
| ... | @@ -72,11 +114,25 @@ index ac8b76830d468edfbc29052b452a36221323c3de..589e329d2aa706ddfff0d1037df0d85a | ... | @@ -72,11 +114,25 @@ index ac8b76830d468edfbc29052b452a36221323c3de..589e329d2aa706ddfff0d1037df0d85a |
| 72 | /** | 114 | /** |
| 73 | * Value indicating if the swipeable panel can be pulled further than the left | 115 | * Value indicating if the swipeable panel can be pulled further than the left |
| 74 | * actions panel's width. It is set to true by default as long as the left | 116 | * actions panel's width. It is set to true by default as long as the left |
| 117 | @@ -90,6 +97,13 @@ export interface SwipeableProps { | ||
| 118 | * Called when action panel is closed. | ||
| 119 | */ | ||
| 120 | onSwipeableClose?: (direction: SwipeDirection.LEFT | SwipeDirection.RIGHT) => void; | ||
| 121 | + /** | ||
| 122 | + * UI-thread worklet called on release before the built-in spring starts. | ||
| 123 | + * Return true to consume the release and take ownership of translation. | ||
| 124 | + * The caller must reset or remove the row after its custom animation. | ||
| 125 | + */ | ||
| 126 | + onSwipeableRelease?: (translation: SharedValue<number>) => boolean; | ||
| 127 | + | ||
| 128 | /** | ||
| 129 | * Called when action panel starts animating on open (either right or left). | ||
| 130 | */ | ||
| 75 | diff --git a/src/components/ReanimatedSwipeable/ReanimatedSwipeable.tsx b/src/components/ReanimatedSwipeable/ReanimatedSwipeable.tsx | 131 | diff --git a/src/components/ReanimatedSwipeable/ReanimatedSwipeable.tsx b/src/components/ReanimatedSwipeable/ReanimatedSwipeable.tsx |
| 76 | index b6134c908624adc590ebd264e90e558b88e235d5..41535348e937ad7762bd531d5b63ba6e092ea997 100644 | 132 | index b6134c908624adc590ebd264e90e558b88e235d5..fa4b1c1749bd0e86cbf5f5546f295d8099e1a481 100644 |
| 77 | --- a/src/components/ReanimatedSwipeable/ReanimatedSwipeable.tsx | 133 | --- a/src/components/ReanimatedSwipeable/ReanimatedSwipeable.tsx |
| 78 | +++ b/src/components/ReanimatedSwipeable/ReanimatedSwipeable.tsx | 134 | +++ b/src/components/ReanimatedSwipeable/ReanimatedSwipeable.tsx |
| 79 | @@ -58,6 +58,7 @@ const Swipeable = (props: SwipeableProps) => { | 135 | @@ -58,11 +58,13 @@ const Swipeable = (props: SwipeableProps) => { |
| 80 | enableTrackpadTwoFingerGesture = DEFAULT_ENABLE_TRACKING_TWO_FINGER_GESTURE, | 136 | enableTrackpadTwoFingerGesture = DEFAULT_ENABLE_TRACKING_TWO_FINGER_GESTURE, |
| 81 | dragOffsetFromLeftEdge = DEFAULT_DRAG_OFFSET, | 137 | dragOffsetFromLeftEdge = DEFAULT_DRAG_OFFSET, |
| 82 | dragOffsetFromRightEdge = DEFAULT_DRAG_OFFSET, | 138 | dragOffsetFromRightEdge = DEFAULT_DRAG_OFFSET, |
| ... | @@ -84,7 +140,34 @@ index b6134c908624adc590ebd264e90e558b88e235d5..41535348e937ad7762bd531d5b63ba6e | ... | @@ -84,7 +140,34 @@ index b6134c908624adc590ebd264e90e558b88e235d5..41535348e937ad7762bd531d5b63ba6e |
| 84 | friction = DEFAULT_FRICTION, | 140 | friction = DEFAULT_FRICTION, |
| 85 | overshootFriction = DEFAULT_OVERSHOOT_FRICTION, | 141 | overshootFriction = DEFAULT_OVERSHOOT_FRICTION, |
| 86 | onSwipeableOpenStartDrag, | 142 | onSwipeableOpenStartDrag, |
| 87 | @@ -537,6 +538,10 @@ const Swipeable = (props: SwipeableProps) => { | 143 | onSwipeableCloseStartDrag, |
| 144 | onSwipeableWillOpen, | ||
| 145 | + onSwipeableRelease, | ||
| 146 | onSwipeableWillClose, | ||
| 147 | onSwipeableOpen, | ||
| 148 | onSwipeableClose, | ||
| 149 | @@ -457,6 +459,11 @@ const Swipeable = (props: SwipeableProps) => { | ||
| 150 | } | ||
| 151 | } | ||
| 152 | |||
| 153 | + // Let a UI-thread full-swipe commit take over before the return spring. | ||
| 154 | + if (onSwipeableRelease?.(appliedTranslation)) { | ||
| 155 | + return; | ||
| 156 | + } | ||
| 157 | + | ||
| 158 | animateRow(toValue, velocityX / friction); | ||
| 159 | }, | ||
| 160 | [ | ||
| 161 | @@ -468,6 +475,8 @@ const Swipeable = (props: SwipeableProps) => { | ||
| 162 | rightWidth, | ||
| 163 | rowState, | ||
| 164 | userDrag, | ||
| 165 | + onSwipeableRelease, | ||
| 166 | + appliedTranslation, | ||
| 167 | ] | ||
| 168 | ); | ||
| 169 | |||
| 170 | @@ -537,6 +546,10 @@ const Swipeable = (props: SwipeableProps) => { | ||
| 88 | dragStarted.value = false; | 171 | dragStarted.value = false; |
| 89 | }); | 172 | }); |
| 90 | 173 | ||
| ... | @@ -95,7 +178,7 @@ index b6134c908624adc590ebd264e90e558b88e235d5..41535348e937ad7762bd531d5b63ba6e | ... | @@ -95,7 +178,7 @@ index b6134c908624adc590ebd264e90e558b88e235d5..41535348e937ad7762bd531d5b63ba6e |
| 95 | Object.entries(relationProps).forEach(([relationName, relation]) => { | 178 | Object.entries(relationProps).forEach(([relationName, relation]) => { |
| 96 | applyRelationProp( | 179 | applyRelationProp( |
| 97 | pan, | 180 | pan, |
| 98 | @@ -552,6 +557,7 @@ const Swipeable = (props: SwipeableProps) => { | 181 | @@ -552,6 +565,7 @@ const Swipeable = (props: SwipeableProps) => { |
| 99 | enableTrackpadTwoFingerGesture, | 182 | enableTrackpadTwoFingerGesture, |
| 100 | dragOffsetFromRightEdge, | 183 | dragOffsetFromRightEdge, |
| 101 | dragOffsetFromLeftEdge, | 184 | dragOffsetFromLeftEdge, |
| ... | @@ -104,7 +187,7 @@ index b6134c908624adc590ebd264e90e558b88e235d5..41535348e937ad7762bd531d5b63ba6e | ... | @@ -104,7 +187,7 @@ index b6134c908624adc590ebd264e90e558b88e235d5..41535348e937ad7762bd531d5b63ba6e |
| 104 | relationProps, | 187 | relationProps, |
| 105 | userDrag, | 188 | userDrag, |
| 106 | diff --git a/src/components/ReanimatedSwipeable/ReanimatedSwipeableProps.ts b/src/components/ReanimatedSwipeable/ReanimatedSwipeableProps.ts | 189 | diff --git a/src/components/ReanimatedSwipeable/ReanimatedSwipeableProps.ts b/src/components/ReanimatedSwipeable/ReanimatedSwipeableProps.ts |
| 107 | index 0c0e517e0d340faf50ff78c3d48e7a2bbcf808ec..d4b6ff508077728c8be83762923d866dc95b144c 100644 | 190 | index 0c0e517e0d340faf50ff78c3d48e7a2bbcf808ec..3ab14d240da346b1927792e480f5b1e902b03bf8 100644 |
| 108 | --- a/src/components/ReanimatedSwipeable/ReanimatedSwipeableProps.ts | 191 | --- a/src/components/ReanimatedSwipeable/ReanimatedSwipeableProps.ts |
| 109 | +++ b/src/components/ReanimatedSwipeable/ReanimatedSwipeableProps.ts | 192 | +++ b/src/components/ReanimatedSwipeable/ReanimatedSwipeableProps.ts |
| 110 | @@ -77,6 +77,14 @@ export interface SwipeableProps { | 193 | @@ -77,6 +77,14 @@ export interface SwipeableProps { |
| ... | @@ -122,3 +205,17 @@ index 0c0e517e0d340faf50ff78c3d48e7a2bbcf808ec..d4b6ff508077728c8be83762923d866d | ... | @@ -122,3 +205,17 @@ index 0c0e517e0d340faf50ff78c3d48e7a2bbcf808ec..d4b6ff508077728c8be83762923d866d |
| 122 | /** | 205 | /** |
| 123 | * Value indicating if the swipeable panel can be pulled further than the left | 206 | * Value indicating if the swipeable panel can be pulled further than the left |
| 124 | * actions panel's width. It is set to true by default as long as the left | 207 | * actions panel's width. It is set to true by default as long as the left |
| 208 | @@ -112,6 +120,13 @@ export interface SwipeableProps { | ||
| 209 | direction: SwipeDirection.LEFT | SwipeDirection.RIGHT | ||
| 210 | ) => void; | ||
| 211 | |||
| 212 | + /** | ||
| 213 | + * UI-thread worklet called on release before the built-in spring starts. | ||
| 214 | + * Return true to consume the release and take ownership of translation. | ||
| 215 | + * The caller must reset or remove the row after its custom animation. | ||
| 216 | + */ | ||
| 217 | + onSwipeableRelease?: (translation: SharedValue<number>) => boolean; | ||
| 218 | + | ||
| 219 | /** | ||
| 220 | * Called when action panel starts animating on open (either right or left). | ||
| 221 | */ |
pnpm-lock.yaml+3-3| ... | @@ -99,7 +99,7 @@ patchedDependencies: | ... | @@ -99,7 +99,7 @@ patchedDependencies: |
| 99 | effect@4.0.0-beta.103: af36b7948b6f9c56623074662b51dade5699880c1a7c71245de73e13c3185fb6 | 99 | effect@4.0.0-beta.103: af36b7948b6f9c56623074662b51dade5699880c1a7c71245de73e13c3185fb6 |
| 100 | expo-audio@57.0.4: fa9a3e0442ed395d4071bb406e08c3a471c9a84700bdfa0b9ad7ff144c96041a | 100 | expo-audio@57.0.4: fa9a3e0442ed395d4071bb406e08c3a471c9a84700bdfa0b9ad7ff144c96041a |
| 101 | expo-sharing@57.0.17: 8d2e3b10eb3f52036a9a086800180ec6cebf3b75bccc5b1775117a7244d4ac45 | 101 | expo-sharing@57.0.17: 8d2e3b10eb3f52036a9a086800180ec6cebf3b75bccc5b1775117a7244d4ac45 |
| 102 | react-native-gesture-handler@2.32.0: 808eb26f9e57cf4945efd3985af4d9c764da6f91f4c9764433cc868602bbf4d3 | 102 | react-native-gesture-handler@2.32.0: 96573c000f7fe56b5abfa13e2e5f0d065907e674cb8e2300155226d7c9874398 |
| 103 | react-native-keyboard-controller@1.21.13: 6e4339347bc5bb3c9ea67d85ff5c814058b211c5750f247aba59d07869a2e787 | 103 | react-native-keyboard-controller@1.21.13: 6e4339347bc5bb3c9ea67d85ff5c814058b211c5750f247aba59d07869a2e787 |
| 104 | react-native-nitro-modules@0.35.9: 825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675 | 104 | react-native-nitro-modules@0.35.9: 825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675 |
| 105 | react-native-screens@4.26.2: 8156dd0f3407822404793cfdaa95639a36b62102f4507c981b8be83600bb382d | 105 | react-native-screens@4.26.2: 8156dd0f3407822404793cfdaa95639a36b62102f4507c981b8be83600bb382d |
| ... | @@ -410,7 +410,7 @@ importers: | ... | @@ -410,7 +410,7 @@ importers: |
| 410 | version: 0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) | 410 | version: 0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) |
| 411 | react-native-gesture-handler: | 411 | react-native-gesture-handler: |
| 412 | specifier: ~2.32.0 | 412 | specifier: ~2.32.0 |
| 413 | version: 2.32.0(patch_hash=808eb26f9e57cf4945efd3985af4d9c764da6f91f4c9764433cc868602bbf4d3)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) | 413 | version: 2.32.0(patch_hash=96573c000f7fe56b5abfa13e2e5f0d065907e674cb8e2300155226d7c9874398)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) |
| 414 | react-native-image-viewing: | 414 | react-native-image-viewing: |
| 415 | specifier: ^0.2.2 | 415 | specifier: ^0.2.2 |
| 416 | version: 0.2.2(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) | 416 | version: 0.2.2(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) |
| ... | @@ -20045,7 +20045,7 @@ snapshots: | ... | @@ -20045,7 +20045,7 @@ snapshots: |
| 20045 | transitivePeerDependencies: | 20045 | transitivePeerDependencies: |
| 20046 | - supports-color | 20046 | - supports-color |
| 20047 | 20047 | ||
| 20048 | react-native-gesture-handler@2.32.0(patch_hash=808eb26f9e57cf4945efd3985af4d9c764da6f91f4c9764433cc868602bbf4d3)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): | 20048 | react-native-gesture-handler@2.32.0(patch_hash=96573c000f7fe56b5abfa13e2e5f0d065907e674cb8e2300155226d7c9874398)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): |
| 20049 | dependencies: | 20049 | dependencies: |
| 20050 | '@egjs/hammerjs': 2.0.17 | 20050 | '@egjs/hammerjs': 2.0.17 |
| 20051 | '@types/react-test-renderer': 19.1.0 | 20051 | '@types/react-test-renderer': 19.1.0 |