From 038cabedaf4691cd657f5119b17455d464d9c0a2 Mon Sep 17 00:00:00 2001 From: Nuno Date: Mon, 27 Apr 2026 16:40:50 +0200 Subject: [PATCH] fix: animations got lost (#8504) --- components/PasswordInput.tsx | 22 +++--- components/TooltipMenu.tsx | 2 - components/TransactionListItem.tsx | 110 +++++++++++++++++++---------- screen/UnlockWith.tsx | 51 +++++++++++-- 4 files changed, 128 insertions(+), 57 deletions(-) diff --git a/components/PasswordInput.tsx b/components/PasswordInput.tsx index cfa79ddfb..d2b3a6c25 100644 --- a/components/PasswordInput.tsx +++ b/components/PasswordInput.tsx @@ -45,27 +45,27 @@ export const PasswordInput = forwardRef // macOS-style shake animation - quick and snappy Animated.sequence([ Animated.timing(shakeAnimation, { - toValue: 20, - duration: 80, - easing: Easing.linear, + toValue: 10, + duration: 50, + easing: Easing.out(Easing.quad), useNativeDriver: true, }), Animated.timing(shakeAnimation, { - toValue: -20, - duration: 80, - easing: Easing.linear, + toValue: -10, + duration: 50, + easing: Easing.out(Easing.quad), useNativeDriver: true, }), Animated.timing(shakeAnimation, { - toValue: 20, - duration: 80, - easing: Easing.linear, + toValue: 8, + duration: 45, + easing: Easing.out(Easing.quad), useNativeDriver: true, }), Animated.timing(shakeAnimation, { toValue: 0, - duration: 80, - easing: Easing.linear, + duration: 45, + easing: Easing.out(Easing.quad), useNativeDriver: true, }), ]).start(() => { diff --git a/components/TooltipMenu.tsx b/components/TooltipMenu.tsx index 79c6cacf7..3fd9c013a 100644 --- a/components/TooltipMenu.tsx +++ b/components/TooltipMenu.tsx @@ -175,8 +175,6 @@ const ToolTipMenu = (props: ToolTipMenuProps) => { { - // Keep visual feedback on Android by default. iOS context-menu preview - // already applies a system press effect; opt in when needed. const shouldApplyPressedStyle = pressed && ((Platform.OS === 'android' && enableAndroidRipple) || (Platform.OS === 'ios' && enableIOSPressOpacity)); return StyleSheet.flatten([styles.pressable, style, buttonStyle, shouldApplyPressedStyle && styles.pressed]); diff --git a/components/TransactionListItem.tsx b/components/TransactionListItem.tsx index bec36aa2e..b30bfe0ac 100644 --- a/components/TransactionListItem.tsx +++ b/components/TransactionListItem.tsx @@ -1,7 +1,7 @@ -import React, { useCallback, useMemo, memo } from 'react'; +import React, { useCallback, useMemo, memo, useRef } from 'react'; import AsyncStorage from '@react-native-async-storage/async-storage'; import Clipboard from '@react-native-clipboard/clipboard'; -import { Linking, Text, TextStyle, ViewStyle, StyleSheet, View } from 'react-native'; +import { Animated, Easing, Linking, Pressable, Text, TextStyle, ViewStyle, StyleSheet, View } from 'react-native'; import Lnurl from '../class/lnurl'; import { LightningTransaction, Transaction } from '../class/wallets/types'; import TransactionExpiredIcon from '../components/icons/TransactionExpiredIcon'; @@ -28,11 +28,6 @@ import { uint8ArrayToHex } from '../blue_modules/uint8array-extras'; import ListItem from './ListItem'; const styles = StyleSheet.create({ - pressable: { - paddingVertical: 12, - borderBottomWidth: StyleSheet.hairlineWidth, - width: '100%', - }, dateLine: { fontSize: 13, }, @@ -70,8 +65,45 @@ const styles = StyleSheet.create({ rightTitle: { textAlign: 'right', }, + animatedScaleContainer: { + width: '100%', + }, }); +type AnimatedPressableRowProps = { + onPress: () => void; + children: React.ReactNode; + accessibilityLabel: string; +}; + +const AnimatedPressableRow: React.FC = ({ onPress, children, accessibilityLabel }) => { + const scaleAnim = useRef(new Animated.Value(1)).current; + + const animateTo = useCallback( + (toValue: number) => { + Animated.timing(scaleAnim, { + toValue, + duration: 120, + easing: Easing.out(Easing.cubic), + useNativeDriver: true, + }).start(); + }, + [scaleAnim], + ); + + return ( + animateTo(0.97)} + onPressOut={() => animateTo(1)} + accessibilityRole="button" + accessibilityLabel={accessibilityLabel} + > + {children} + + ); +}; + interface TransactionListItemProps { itemPriceUnit?: BitcoinUnit; walletID: string; @@ -443,45 +475,45 @@ export const TransactionListItem: React.FC = memo( return ( - {/* @ts-ignore - Context menu wrapper types can be overly strict about child element props */} - {dateLine}} - chevron={false} - rightTitle={rowTitle} - rightTitleStyle={rowTitleStyle} - rightSubtitle={noteForCopy} - rightSubtitleStyle={styles.rightColumn} - containerStyle={combinedStyle} - testID="TransactionListItem" - accessibilityRole="button" - accessibilityLabel={`${transactionTypeLabel}, ${amountWithUnit}, ${subtitle ?? title}`} - > - - {avatar} - - - {title} - - {subtitleContent} + + {/* @ts-ignore - Context menu wrapper types can be overly strict about child element props */} + {dateLine}} + chevron={false} + rightTitle={rowTitle} + rightTitleStyle={rowTitleStyle} + rightSubtitle={noteForCopy} + rightSubtitleStyle={styles.rightColumn} + containerStyle={combinedStyle} + testID="TransactionListItem" + accessibilityRole="button" + accessibilityLabel={`${transactionTypeLabel}, ${amountWithUnit}, ${subtitle ?? title}`} + > + + {avatar} + + + {title} + + {subtitleContent} + + + + {rowTitle} + + - - - {rowTitle} - - - - + + ); }, diff --git a/screen/UnlockWith.tsx b/screen/UnlockWith.tsx index b9c1bf176..904fd7c20 100644 --- a/screen/UnlockWith.tsx +++ b/screen/UnlockWith.tsx @@ -1,9 +1,11 @@ import React, { useCallback, useEffect, useReducer, useRef } from 'react'; import { ActivityIndicator, + Animated, + Dimensions, + Easing, Image, Keyboard, - KeyboardAvoidingView, Platform, StyleSheet, TouchableWithoutFeedback, @@ -80,6 +82,7 @@ function reducer(state: State, action: Action): State { const UnlockWith: React.FC = () => { const [state, dispatch] = useReducer(reducer, initialState); const isUnlockingWallets = useRef(false); + const keyboardOffset = useRef(new Animated.Value(0)).current; const passwordInputRef = useRef(null); const passwordResolveRef = useRef<((password: string | undefined) => void) | null>(null); const { setWalletsInitialized, isStorageEncrypted, startAndDecrypt } = useStorage(); @@ -89,6 +92,44 @@ const UnlockWith: React.FC = () => { setWalletsInitialized(false); }, [setWalletsInitialized]); + useEffect(() => { + const windowHeight = Dimensions.get('window').height; + + const animateToKeyboardPosition = (event: any, fallbackDuration = 220) => { + const keyboardTop = event?.endCoordinates?.screenY ?? windowHeight; + const keyboardHeight = Math.max(0, windowHeight - keyboardTop); + const target = -Math.min(Math.max(keyboardHeight * 0.28, 0), 96); + + Animated.timing(keyboardOffset, { + toValue: target, + duration: event?.duration ?? fallbackDuration, + easing: Easing.out(Easing.cubic), + useNativeDriver: true, + }).start(); + }; + + const resetPosition = (event?: any) => { + Animated.timing(keyboardOffset, { + toValue: 0, + duration: event?.duration ?? 220, + easing: Easing.out(Easing.cubic), + useNativeDriver: true, + }).start(); + }; + + const subscriptions = + Platform.OS === 'ios' + ? [ + Keyboard.addListener('keyboardWillChangeFrame', animateToKeyboardPosition), + Keyboard.addListener('keyboardWillHide', resetPosition), + ] + : [Keyboard.addListener('keyboardDidShow', animateToKeyboardPosition), Keyboard.addListener('keyboardDidHide', resetPosition)]; + + return () => { + subscriptions.forEach(sub => sub.remove()); + }; + }, [keyboardOffset]); + const successfullyAuthenticated = useCallback(() => { setWalletsInitialized(true); isUnlockingWallets.current = false; @@ -244,14 +285,14 @@ const UnlockWith: React.FC = () => { return ( - - + + {renderUnlockOptions()} - - + + );