From 7378e488770d6d594fb44daa73e809d235bbb40b Mon Sep 17 00:00:00 2001 From: Justin Wyne <1986068+wyne@users.noreply.github.com> Date: Wed, 10 Jun 2026 01:21:36 -0400 Subject: [PATCH 1/2] Defer swipe score commits until gesture end --- .../Interactions/Swipe/Swipe.test.tsx | 43 ++++--- src/components/Interactions/Swipe/Swipe.tsx | 117 +++++++++++------- .../PlayerTiles/AdditionTile/AdditionTile.tsx | 13 +- .../AdditionTile/AnimatedScoreText.tsx | 33 +++++ .../AdditionTile/OptimisticScoreContext.ts | 10 ++ .../PlayerTiles/AdditionTile/ScoreAfter.tsx | 36 +++++- .../PlayerTiles/AdditionTile/ScoreBefore.tsx | 40 ++++-- .../PlayerTiles/AdditionTile/ScoreRound.tsx | 41 +++--- 8 files changed, 242 insertions(+), 91 deletions(-) create mode 100644 src/components/PlayerTiles/AdditionTile/AnimatedScoreText.tsx create mode 100644 src/components/PlayerTiles/AdditionTile/OptimisticScoreContext.ts diff --git a/src/components/Interactions/Swipe/Swipe.test.tsx b/src/components/Interactions/Swipe/Swipe.test.tsx index 2af0c334..4b65cc8d 100644 --- a/src/components/Interactions/Swipe/Swipe.test.tsx +++ b/src/components/Interactions/Swipe/Swipe.test.tsx @@ -13,10 +13,6 @@ jest.mock('react-native-reanimated', () => ({ __esModule: true, useSharedValue: function (i: unknown) { return { value: i }; }, useAnimatedStyle: function (fn: () => unknown) { return fn(); }, - useAnimatedReaction: function (_p: () => unknown, r: (c: unknown, p: unknown) => void) { - (globalThis as any).__react = r; - r(_p(), undefined); - }, runOnJS: function (fn: (...args: unknown[]) => unknown) { return fn; }, withTiming: function (_t: number) { return _t; }, default: { View: function () { return null; } }, @@ -56,7 +52,6 @@ const stub = function (s: any = {}) { return s; }; beforeEach(() => { (globalThis as any).__ph = {}; - (globalThis as any).__react = undefined; jest.useRealTimers(); }); @@ -78,11 +73,6 @@ const renderSwipe = () => { return { dispatch }; }; -const triggerReaction = (totalOffset: number, prevTotalOffset: number) => { - const react = (globalThis as any).__react; - if (react) react(totalOffset, prevTotalOffset); -}; - describe('SwipeVertical', () => { it('renders and captures gesture callbacks', () => { renderSwipe(); @@ -98,7 +88,10 @@ describe('SwipeVertical', () => { ph.onBegin(); ph.onUpdate({ translationY: -100 }); - triggerReaction(100, 0); + expect(dispatch).not.toHaveBeenCalledWith( + playerRoundScoreIncrement('player-1', 0, 2) + ); + act(() => { ph.onEnd({ translationY: -100 }); ph.onFinalize(); @@ -115,7 +108,6 @@ describe('SwipeVertical', () => { ph.onBegin(); ph.onUpdate({ translationY: 100 }); - triggerReaction(-100, 0); act(() => { ph.onEnd({ translationY: 100 }); ph.onFinalize(); @@ -132,7 +124,6 @@ describe('SwipeVertical', () => { ph.onBegin(); ph.onUpdate({ translationY: -250 }); - triggerReaction(250, 0); act(() => { ph.onEnd({ translationY: -250 }); ph.onFinalize(); @@ -152,7 +143,6 @@ describe('SwipeVertical', () => { act(() => { jest.advanceTimersByTime(401); }); ph.onUpdate({ translationY: -100 }); - triggerReaction(100, 0); act(() => { ph.onEnd({ translationY: -100 }); ph.onFinalize(); @@ -170,12 +160,10 @@ describe('SwipeVertical', () => { ph.onBegin(); ph.onUpdate({ translationY: -2 }); - triggerReaction(2, 0); act(() => { jest.advanceTimersByTime(401); }); ph.onUpdate({ translationY: -100 }); - triggerReaction(100, 2); act(() => { ph.onEnd({ translationY: -100 }); ph.onFinalize(); @@ -185,4 +173,27 @@ describe('SwipeVertical', () => { playerRoundScoreIncrement('player-1', 0, 2) ); }); + + it('flushes only once when onEnd and onFinalize both run', () => { + const { dispatch } = renderSwipe(); + const ph = (globalThis as any).__ph; + const expectedAction = playerRoundScoreIncrement('player-1', 0, 2); + + ph.onBegin(); + ph.onUpdate({ translationY: -100 }); + act(() => { + ph.onEnd({ translationY: -100 }); + ph.onFinalize(); + }); + + expect(dispatch.mock.calls.filter(([action]) => { + const dispatchedAction = action as typeof expectedAction; + return ( + dispatchedAction.type === expectedAction.type && + dispatchedAction.payload === expectedAction.payload && + dispatchedAction.meta.round === expectedAction.meta.round && + dispatchedAction.meta.multiplier === expectedAction.meta.multiplier + ); + })).toHaveLength(1); + }); }); diff --git a/src/components/Interactions/Swipe/Swipe.tsx b/src/components/Interactions/Swipe/Swipe.tsx index 09d794b7..6a0b0165 100644 --- a/src/components/Interactions/Swipe/Swipe.tsx +++ b/src/components/Interactions/Swipe/Swipe.tsx @@ -1,15 +1,16 @@ -import React, { useCallback, useEffect, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import * as Haptics from 'expo-haptics'; import { Animated, StyleSheet, Text, View } from 'react-native'; import { Gesture, GestureDetector } from 'react-native-gesture-handler'; -import ReAnimated, { runOnJS, useAnimatedReaction, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; +import ReAnimated, { runOnJS, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; import { useAppDispatch, useAppSelector } from '../../../../redux/hooks'; -import { playerRoundScoreIncrement } from '../../../../redux/PlayersSlice'; +import { playerRoundScoreIncrement, selectPlayerRoundStats } from '../../../../redux/PlayersSlice'; import { selectCurrentGame } from '../../../../redux/selectors'; import { logEvent } from '../../../Analytics'; import { useMenuOpen } from '../../MenuOpenContext'; +import { OptimisticScoreContext } from '../../PlayerTiles/AdditionTile/OptimisticScoreContext'; interface HalfTapProps { children: React.ReactNode; @@ -38,11 +39,32 @@ const SwipeVertical: React.FC = ({ const currentGameId = useAppSelector(state => state.settings.currentGameId); const currentRoundIndex = useAppSelector(state => selectCurrentGame(state)?.roundCurrent) || 0; const currentGameLocked = useAppSelector(state => selectCurrentGame(state)?.locked); + const { currentRoundScore, currentRoundTotalScore } = useAppSelector( + state => selectPlayerRoundStats(state, playerId, currentRoundIndex) + ); const dispatch = useAppDispatch(); const addendOne = useAppSelector(state => state.settings.addendOne); const addendTwo = useAppSelector(state => state.settings.addendTwo); + const svAddendOne = useSharedValue(addendOne); + const svAddendTwo = useSharedValue(addendTwo); + const svRoundScore = useSharedValue(currentRoundScore); + const svRoundTotalScore = useSharedValue(currentRoundTotalScore); + const optimisticScores = useMemo(() => ({ + currentRoundScore: svRoundScore, + currentRoundTotalScore: svRoundTotalScore, + }), [svRoundScore, svRoundTotalScore]); + + useEffect(() => { + svAddendOne.value = addendOne; + svAddendTwo.value = addendTwo; + }, [addendOne, addendTwo]); + + useLayoutEffect(() => { + svRoundScore.value = currentRoundScore; + svRoundTotalScore.value = currentRoundTotalScore; + }, [currentRoundScore, currentRoundTotalScore]); //#endregion @@ -131,8 +153,12 @@ const SwipeVertical: React.FC = ({ //#region Gesture handling - const totalOffset = useSharedValue(0); const panY = useSharedValue(0); + const svStartRoundScore = useSharedValue(0); + const svStartRoundTotalScore = useSharedValue(0); + const svPendingDelta = useSharedValue(0); + const svLastNotches = useSharedValue(0); + const svDidFlush = useSharedValue(true); const { menuOpen } = useMenuOpen(); @@ -151,27 +177,68 @@ const SwipeVertical: React.FC = ({ secondaryHoldStop(); }, [index, currentGameId, secondaryHold, addendOne, addendTwo, currentRoundIndex, menuOpen]); + const flushPendingChange = useCallback((delta: number) => { + if (delta === 0) return; + if (menuOpen) return; + + dispatch(playerRoundScoreIncrement(playerId, currentRoundIndex, delta)); + }, [dispatch, playerId, currentRoundIndex, menuOpen]); + + const bumpFeedback = useCallback((secondary: boolean) => { + if (secondary) { + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); + } else { + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + } + }, []); + const panGesture = Gesture.Pan() .enabled(!currentGameLocked && !menuOpen) .minDistance(0) .onBegin(() => { + svStartRoundScore.value = svRoundScore.value; + svStartRoundTotalScore.value = svRoundTotalScore.value; + svPendingDelta.value = 0; + svLastNotches.value = 0; + svDidFlush.value = false; runOnJS(secondaryHoldStart)(); }) .onUpdate((event) => { const y = event.translationY; panY.value = y; - totalOffset.value = -y; if (isSecondaryHoldActive.value == false && Math.abs(y) > 1) { runOnJS(secondaryHoldStop)(); } + + const notches = Math.round((-y || 0) / notchSize); + if (notches === svLastNotches.value) return; + + svLastNotches.value = notches; + const secondaryActive = isSecondaryHoldActive.value; + const scoreDelta = notches * (secondaryActive ? svAddendTwo.value : svAddendOne.value); + svPendingDelta.value = scoreDelta; + svRoundScore.value = svStartRoundScore.value + scoreDelta; + svRoundTotalScore.value = svStartRoundTotalScore.value + scoreDelta; + runOnJS(bumpFeedback)(secondaryActive); }) .onEnd((event) => { - totalOffset.value = null; + if (!svDidFlush.value) { + svDidFlush.value = true; + if (svPendingDelta.value !== 0) { + runOnJS(flushPendingChange)(svPendingDelta.value); + } + } panY.value = withTiming(0, { duration: 200 }); runOnJS(endGesture)(event.translationY); }) .onFinalize(() => { + if (!svDidFlush.value) { + svDidFlush.value = true; + if (svPendingDelta.value !== 0) { + runOnJS(flushPendingChange)(svPendingDelta.value); + } + } runOnJS(secondaryHoldStop)(); }); @@ -181,42 +248,8 @@ const SwipeVertical: React.FC = ({ //#endregion - //#region Helpers - - const scoreChangeHandler = (value: number) => { - if (Math.abs(value) == 0) return; - if (menuOpen) return; - - const scoreDelta = value * (secondaryHold ? addendTwo : addendOne); - - if (secondaryHold) { - Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); - } else { - Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); - } - - dispatch(playerRoundScoreIncrement(playerId, currentRoundIndex, scoreDelta)); - }; - - useAnimatedReaction( - () => { - return totalOffset.value; - }, - (currentValue, previousValue) => { - if (currentValue === null) return; - - const c = Math.round((currentValue || 0) / notchSize); - const p = Math.round((previousValue || 0) / notchSize); - if (c - p !== 0) { - runOnJS(scoreChangeHandler)(c - p); - } - } - ); - - //#endregion - return ( - <> + = ({ - + ); }; diff --git a/src/components/PlayerTiles/AdditionTile/AdditionTile.tsx b/src/components/PlayerTiles/AdditionTile/AdditionTile.tsx index 3e29c115..a9da8496 100644 --- a/src/components/PlayerTiles/AdditionTile/AdditionTile.tsx +++ b/src/components/PlayerTiles/AdditionTile/AdditionTile.tsx @@ -8,6 +8,7 @@ import { selectPlayerById, selectPlayerRoundStats } from '../../../../redux/Play import { selectCurrentGame } from '../../../../redux/selectors'; import { calculateFontSize } from './Helpers'; +import { OptimisticScoreContext } from './OptimisticScoreContext'; import ScoreAfter from './ScoreAfter'; import ScoreBefore from './ScoreBefore'; import ScoreRound from './ScoreRound'; @@ -39,6 +40,7 @@ const AdditionTile: React.FunctionComponent = ({ const { currentRoundScore, currentRoundTotalScore, grandTotalScore } = useAppSelector( state => selectPlayerRoundStats(state, playerId, currentRoundIndex) ); + const optimisticScores = React.useContext(OptimisticScoreContext); if (maxWidth == null || maxHeight == null) return null; @@ -76,13 +78,18 @@ const AdditionTile: React.FunctionComponent = ({ + fontColor={fontColor} + optimisticCurrentRoundScore={optimisticScores?.currentRoundScore} + optimisticCurrentRoundTotalScore={optimisticScores?.currentRoundTotalScore} /> + fontColor={fontColor} + optimisticCurrentRoundScore={optimisticScores?.currentRoundScore} /> + fontColor={fontColor} + optimisticCurrentRoundScore={optimisticScores?.currentRoundScore} + optimisticCurrentRoundTotalScore={optimisticScores?.currentRoundTotalScore} /> ); }; diff --git a/src/components/PlayerTiles/AdditionTile/AnimatedScoreText.tsx b/src/components/PlayerTiles/AdditionTile/AnimatedScoreText.tsx new file mode 100644 index 00000000..8f165881 --- /dev/null +++ b/src/components/PlayerTiles/AdditionTile/AnimatedScoreText.tsx @@ -0,0 +1,33 @@ +import React from 'react'; + +import { TextInput, TextInputProps } from 'react-native'; +import Animated, { SharedValue, useAnimatedProps } from 'react-native-reanimated'; + +type AnimatedTextInputProps = TextInputProps & { text?: string }; + +const AnimatedTextInput = Animated.createAnimatedComponent( + TextInput as React.ComponentType +); + +interface Props extends Omit { + text: SharedValue; +} + +const AnimatedScoreText: React.FC = ({ text, ...props }) => { + const animatedProps = useAnimatedProps(() => ({ + text: text.value, + })); + + return ( + + ); +}; + +export default AnimatedScoreText; diff --git a/src/components/PlayerTiles/AdditionTile/OptimisticScoreContext.ts b/src/components/PlayerTiles/AdditionTile/OptimisticScoreContext.ts new file mode 100644 index 00000000..fb8edaa6 --- /dev/null +++ b/src/components/PlayerTiles/AdditionTile/OptimisticScoreContext.ts @@ -0,0 +1,10 @@ +import React from 'react'; + +import { SharedValue } from 'react-native-reanimated'; + +export interface OptimisticScoreValues { + currentRoundScore: SharedValue; + currentRoundTotalScore: SharedValue; +} + +export const OptimisticScoreContext = React.createContext(null); diff --git a/src/components/PlayerTiles/AdditionTile/ScoreAfter.tsx b/src/components/PlayerTiles/AdditionTile/ScoreAfter.tsx index 338d3747..b14f97b6 100644 --- a/src/components/PlayerTiles/AdditionTile/ScoreAfter.tsx +++ b/src/components/PlayerTiles/AdditionTile/ScoreAfter.tsx @@ -1,11 +1,14 @@ import React, { useEffect } from 'react'; import Animated, { + SharedValue, + useDerivedValue, useSharedValue, useAnimatedStyle, withTiming } from 'react-native-reanimated'; +import AnimatedScoreText from './AnimatedScoreText'; import { calculateFontSize, animationDuration, enteringAnimation, ZoomOutFadeOut } from './Helpers'; import { scoreStyles } from './scoreStyles'; @@ -14,20 +17,36 @@ interface Props { currentRoundTotalScore: number; fontColor: string; containerWidth: number; + optimisticCurrentRoundScore?: SharedValue; + optimisticCurrentRoundTotalScore?: SharedValue; } -const ScoreAfter: React.FunctionComponent = ({ containerWidth, currentRoundScore, currentRoundTotalScore, fontColor }) => { +const ScoreAfter: React.FunctionComponent = ({ + containerWidth, + currentRoundScore, + currentRoundTotalScore, + fontColor, + optimisticCurrentRoundScore, + optimisticCurrentRoundTotalScore, +}) => { + const fallbackRoundScore = useSharedValue(currentRoundScore); + const fallbackRoundTotalScore = useSharedValue(currentRoundTotalScore); + const roundScore = optimisticCurrentRoundScore ?? fallbackRoundScore; + const roundTotalScore = optimisticCurrentRoundTotalScore ?? fallbackRoundTotalScore; const fontSize = useSharedValue(calculateFontSize(containerWidth)); const opacity = useSharedValue(1); + const text = useDerivedValue(() => String(roundTotalScore.value)); const animatedStyles = useAnimatedStyle(() => { return { fontSize: fontSize.value, - opacity: opacity.value, + opacity: optimisticCurrentRoundScore ? (roundScore.value === 0 ? 0 : 1) : opacity.value, }; }); useEffect(() => { + fallbackRoundScore.value = currentRoundScore; + fallbackRoundTotalScore.value = currentRoundTotalScore; fontSize.value = withTiming( currentRoundScore == 0 ? 1 : calculateFontSize(containerWidth) * 1.1, { duration: animationDuration }, @@ -40,11 +59,16 @@ const ScoreAfter: React.FunctionComponent = ({ containerWidth, currentRou return ( - - {currentRoundTotalScore} - + style={[animatedStyles, scoreStyles.scoreText, { + color: fontColor, + padding: 0, + textAlign: 'center', + backgroundColor: 'transparent', + }]} + /> ); }; diff --git a/src/components/PlayerTiles/AdditionTile/ScoreBefore.tsx b/src/components/PlayerTiles/AdditionTile/ScoreBefore.tsx index b51e708a..26cf1c4c 100644 --- a/src/components/PlayerTiles/AdditionTile/ScoreBefore.tsx +++ b/src/components/PlayerTiles/AdditionTile/ScoreBefore.tsx @@ -1,11 +1,14 @@ import React, { useEffect } from 'react'; import Animated, { + SharedValue, + useDerivedValue, useSharedValue, useAnimatedStyle, withTiming } from 'react-native-reanimated'; +import AnimatedScoreText from './AnimatedScoreText'; import { calculateFontSize, animationDuration, enteringAnimation, multiLineScoreSizeMultiplier, singleLineScoreSizeMultiplier, scoreMathOpacity } from './Helpers'; interface Props { @@ -13,30 +16,44 @@ interface Props { currentRoundTotalScore: number; fontColor: string; containerWidth: number; + optimisticCurrentRoundScore?: SharedValue; + optimisticCurrentRoundTotalScore?: SharedValue; } const ScoreBefore: React.FunctionComponent = ({ containerWidth, currentRoundScore, currentRoundTotalScore, - fontColor + fontColor, + optimisticCurrentRoundScore, + optimisticCurrentRoundTotalScore, }) => { - const scoreBefore = currentRoundTotalScore - currentRoundScore; - + const fallbackRoundScore = useSharedValue(currentRoundScore); + const fallbackRoundTotalScore = useSharedValue(currentRoundTotalScore); const fontSize = useSharedValue(calculateFontSize(containerWidth)); const fontOpacity = useSharedValue(100); + const roundScore = optimisticCurrentRoundScore ?? fallbackRoundScore; + const roundTotalScore = optimisticCurrentRoundTotalScore ?? fallbackRoundTotalScore; + const hasOptimisticScore = optimisticCurrentRoundScore != null; + const scoreBefore = useDerivedValue(() => String(roundTotalScore.value - roundScore.value)); + const animatedStyles = useAnimatedStyle(() => { + const scaleFactor = roundScore.value == 0 ? singleLineScoreSizeMultiplier : multiLineScoreSizeMultiplier; return { - fontSize: fontSize.value, - fontWeight: currentRoundScore == 0 ? 'bold' : 'normal', - opacity: fontOpacity.value / 100, + fontSize: hasOptimisticScore ? calculateFontSize(containerWidth) * scaleFactor : fontSize.value, + fontWeight: roundScore.value == 0 ? 'bold' : 'normal', + opacity: hasOptimisticScore + ? (roundScore.value == 0 ? 1 : scoreMathOpacity) + : fontOpacity.value / 100, }; }); const scaleFactor = currentRoundScore == 0 ? singleLineScoreSizeMultiplier : multiLineScoreSizeMultiplier; useEffect(() => { + fallbackRoundScore.value = currentRoundScore; + fallbackRoundTotalScore.value = currentRoundTotalScore; fontSize.value = withTiming( calculateFontSize(containerWidth) * scaleFactor, { duration: animationDuration } @@ -50,15 +67,18 @@ const ScoreBefore: React.FunctionComponent = ({ return ( - - {scoreBefore} - + padding: 0, + textAlign: 'center', + backgroundColor: 'transparent', + }]} + /> ); }; diff --git a/src/components/PlayerTiles/AdditionTile/ScoreRound.tsx b/src/components/PlayerTiles/AdditionTile/ScoreRound.tsx index 641ae073..0278529b 100644 --- a/src/components/PlayerTiles/AdditionTile/ScoreRound.tsx +++ b/src/components/PlayerTiles/AdditionTile/ScoreRound.tsx @@ -1,31 +1,47 @@ import React, { useEffect } from 'react'; import Animated, { + SharedValue, + useDerivedValue, useSharedValue, useAnimatedStyle, withTiming } from 'react-native-reanimated'; +import AnimatedScoreText from './AnimatedScoreText'; import { calculateFontSize, animationDuration, multiLineScoreSizeMultiplier, scoreMathOpacity } from './Helpers'; interface Props { currentRoundScore: number; fontColor: string; containerWidth: number; + optimisticCurrentRoundScore?: SharedValue; } -const ScoreRound: React.FunctionComponent = ({ containerWidth, currentRoundScore, fontColor }) => { +const ScoreRound: React.FunctionComponent = ({ + containerWidth, + currentRoundScore, + fontColor, + optimisticCurrentRoundScore, +}) => { + const fallbackRoundScore = useSharedValue(currentRoundScore); + const roundScore = optimisticCurrentRoundScore ?? fallbackRoundScore; const fontSize = useSharedValue(calculateFontSize(containerWidth)); + const text = useDerivedValue(() => { + if (roundScore.value === 0) return ''; + const sign = roundScore.value > 0 ? ' + ' : ' - '; + return sign + Math.abs(roundScore.value); + }); const animatedStyles = useAnimatedStyle(() => { return { fontSize: fontSize.value, + opacity: roundScore.value === 0 ? 0 : scoreMathOpacity, }; }); - const d = currentRoundScore; - useEffect(() => { + fallbackRoundScore.value = currentRoundScore; fontSize.value = withTiming( calculateFontSize(containerWidth) * multiLineScoreSizeMultiplier, { duration: animationDuration } @@ -33,23 +49,20 @@ const ScoreRound: React.FunctionComponent = ({ containerWidth, currentRou }, [currentRoundScore, containerWidth]); - if (currentRoundScore == 0) { - return <>; - } - return ( - - {currentRoundScore > 0 && ' + '} - {currentRoundScore < 0 && ' - '} - {Math.abs(d)} - + padding: 0, + textAlign: 'center', + backgroundColor: 'transparent', + }]} + /> ); }; From a162f5206a24af36141c3c88a7dcc77173ed7d16 Mon Sep 17 00:00:00 2001 From: Justin Wyne <1986068+wyne@users.noreply.github.com> Date: Wed, 10 Jun 2026 01:40:43 -0400 Subject: [PATCH 2/2] Fix animated score text style type --- .../PlayerTiles/AdditionTile/AnimatedScoreText.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/PlayerTiles/AdditionTile/AnimatedScoreText.tsx b/src/components/PlayerTiles/AdditionTile/AnimatedScoreText.tsx index 8f165881..43aec796 100644 --- a/src/components/PlayerTiles/AdditionTile/AnimatedScoreText.tsx +++ b/src/components/PlayerTiles/AdditionTile/AnimatedScoreText.tsx @@ -1,7 +1,7 @@ import React from 'react'; -import { TextInput, TextInputProps } from 'react-native'; -import Animated, { SharedValue, useAnimatedProps } from 'react-native-reanimated'; +import { StyleProp, TextInput, TextInputProps, TextStyle } from 'react-native'; +import Animated, { AnimatedStyle, SharedValue, useAnimatedProps } from 'react-native-reanimated'; type AnimatedTextInputProps = TextInputProps & { text?: string }; @@ -9,7 +9,8 @@ const AnimatedTextInput = Animated.createAnimatedComponent( TextInput as React.ComponentType ); -interface Props extends Omit { +interface Props extends Omit { + style?: StyleProp>; text: SharedValue; }