diff --git a/src/components/expanded-state/asset/ChartExpandedState.js b/src/components/expanded-state/asset/ChartExpandedState.js index 4704316db9b..9f7fe511605 100644 --- a/src/components/expanded-state/asset/ChartExpandedState.js +++ b/src/components/expanded-state/asset/ChartExpandedState.js @@ -40,10 +40,11 @@ import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks import { ChainId } from '@/state/backendNetworks/types'; import { useTimeoutEffect } from '@/hooks/useTimeout'; import { analyticsV2 } from '@/analytics'; +import { IS_ANDROID, IS_IOS } from '@/env'; const defaultCarouselHeight = 60; -const baseHeight = 386 + (android && 20 - getSoftMenuBarHeight()) - defaultCarouselHeight; -const heightWithoutChart = baseHeight + (android && 30); +const baseHeight = 386 + (IS_ANDROID && 20 - getSoftMenuBarHeight()) - defaultCarouselHeight; +const heightWithoutChart = baseHeight + (IS_ANDROID && 30); const heightWithChart = baseHeight + 292; const Carousel = styled.ScrollView.attrs({ @@ -187,24 +188,15 @@ export default function ChartExpandedState({ asset }) { const delayedDescriptions = useDelayedValueWithLayoutAnimation(data?.description?.replace(/\s+/g, '')); - const scrollableContentHeight = true; const { chart, chartType, color, fetchingCharts, updateChartType, initialChartDataLabels, showChart, throttledData } = useChartThrottledPoints({ asset: assetWithPrice, heightWithChart: Math.min( - carouselHeight + - heightWithChart - - (!hasBalance && 68) + - additionalContentHeight + - (additionalContentHeight === 0 ? 0 : scrollableContentHeight), + carouselHeight + heightWithChart - (!hasBalance && 68) + additionalContentHeight + (additionalContentHeight === 0 ? 0 : true), screenHeight ), heightWithoutChart: Math.min( - carouselHeight + - heightWithoutChart - - (!hasBalance && 68) + - additionalContentHeight + - (additionalContentHeight === 0 ? 0 : scrollableContentHeight), + carouselHeight + heightWithoutChart - (!hasBalance && 68) + additionalContentHeight + (additionalContentHeight === 0 ? 0 : true), screenHeight ), shortHeightWithChart: Math.min(carouselHeight + heightWithChart - (!hasBalance && 68), screenHeight), @@ -219,9 +211,9 @@ export default function ChartExpandedState({ asset }) { duration.current = 300; } - let ChartExpandedStateSheetHeight = ios || showChart ? heightWithChart : heightWithoutChart; + let ChartExpandedStateSheetHeight = IS_IOS || showChart ? heightWithChart : heightWithoutChart; - if (android && !hasBalance) { + if (IS_ANDROID && !hasBalance) { ChartExpandedStateSheetHeight -= 60; } @@ -271,10 +263,10 @@ export default function ChartExpandedState({ asset }) { return ( ({ - ...padding.object(0, 19, showChart ? (android ? 15 : 30) : 0), + ...padding.object(0, 19, showChart ? 36 : 0), })); -function useTabularNumsWhileScrubbing() { - const [tabularNums, enable, disable] = useBooleanState(); - // Only enable tabularNums on the price label when the user is scrubbing - // because we are obnoxiously into details - const { isActive } = useChartData(); - - useAnimatedReaction( - () => isActive.value, - useTabularNums => { - runOnJS(useTabularNums ? enable : disable)(); - } - ); - - return tabularNums; -} - export default function ChartExpandedStateHeader({ asset, color: givenColors, - dateRef, isPool, latestChange, latestPrice = noPriceData, @@ -52,7 +37,6 @@ export default function ChartExpandedStateHeader({ const theme = useTheme(); const color = givenColors || theme.colors.dark; const { nativeCurrency } = useAccountSettings(); - const tabularNums = useTabularNumsWhileScrubbing(); const isNoPriceData = latestPrice === noPriceData; @@ -64,8 +48,6 @@ export default function ChartExpandedStateHeader({ const titleOrNoPriceData = isNoPriceData ? noPriceData : title; - const showPriceChange = !isNoPriceData && showChart && !isNaN(latestChange); - const invertedChartTypes = Object.entries(ChartTypes).reduce((acc, [key, value]) => { acc[value] = key; return acc; @@ -101,9 +83,17 @@ export default function ChartExpandedStateHeader({ const firstValue = data?.points?.[0]?.y; const lastValue = data?.points?.[data.points.length - 1]?.y; - return firstValue === Number(firstValue) ? lastValue / firstValue : 1; + return firstValue === Number(firstValue) ? lastValue / firstValue : undefined; }, [data]); + const showPriceChangeStyle = useAnimatedStyle(() => { + const showPriceChange = !isNoPriceData && showChart && !isNaN(latestChange.value); + return { + display: showPriceChange ? 'flex' : 'none', + opacity: withTiming(showPriceChange ? 1 : 0, TIMING_CONFIGS.slowFadeConfig), + }; + }); + return ( @@ -115,38 +105,40 @@ export default function ChartExpandedStateHeader({ theme={theme} colors={asset?.colors} /> - - - - - {showPriceChange && } - - - - - {titleOrNoPriceData} - - {showPriceChange && } - - + + + + + + + + + + + + + + + + + {titleOrNoPriceData} + + + + + + + + + ); } diff --git a/src/components/expanded-state/chart/chart-data-labels/ChartChangeDirectionArrow.js b/src/components/expanded-state/chart/chart-data-labels/ChartChangeDirectionArrow.tsx similarity index 82% rename from src/components/expanded-state/chart/chart-data-labels/ChartChangeDirectionArrow.js rename to src/components/expanded-state/chart/chart-data-labels/ChartChangeDirectionArrow.tsx index 5e4fb86c2ac..3451d3e5518 100644 --- a/src/components/expanded-state/chart/chart-data-labels/ChartChangeDirectionArrow.js +++ b/src/components/expanded-state/chart/chart-data-labels/ChartChangeDirectionArrow.tsx @@ -1,18 +1,15 @@ import MaskedView from '@react-native-masked-view/masked-view'; import React from 'react'; -import Animated, { useAnimatedStyle } from 'react-native-reanimated'; -import { Icon } from '../../../icons'; +import Animated, { DerivedValue, useAnimatedStyle } from 'react-native-reanimated'; import { useChartData } from '@/react-native-animated-charts/src'; -import styled from '@/styled-thing'; +import { Icon } from '../../../icons'; +import { useTheme } from '@/theme'; const AnimatedMaskedView = Animated.createAnimatedComponent(MaskedView); -const ArrowIcon = styled(Icon).attrs({ - direction: 'right', - name: 'fatArrow', -})({}); +const ArrowIcon = () => ; -export default function ChartChangeDirectionArrow({ ratio, sharedRatio }) { +export default function ChartChangeDirectionArrow({ ratio, sharedRatio }: { ratio: number; sharedRatio: DerivedValue }) { const { colors } = useTheme(); const { isActive } = useChartData(); diff --git a/src/components/expanded-state/chart/chart-data-labels/ChartDateLabel.js b/src/components/expanded-state/chart/chart-data-labels/ChartDateLabel.tsx similarity index 64% rename from src/components/expanded-state/chart/chart-data-labels/ChartDateLabel.js rename to src/components/expanded-state/chart/chart-data-labels/ChartDateLabel.tsx index c6b78d507fe..6308b7a450d 100644 --- a/src/components/expanded-state/chart/chart-data-labels/ChartDateLabel.js +++ b/src/components/expanded-state/chart/chart-data-labels/ChartDateLabel.tsx @@ -1,20 +1,9 @@ import lang from 'i18n-js'; -import React from 'react'; -import { View } from 'react-native'; -import { useAnimatedStyle } from 'react-native-reanimated'; +import React, { useCallback } from 'react'; +import Animated, { AnimatedStyle, FadeIn, useAnimatedStyle } from 'react-native-reanimated'; import { useRatio } from './useRatio'; import { ChartXLabel, useChartData } from '@/react-native-animated-charts/src'; -import styled from '@/styled-thing'; -import { fonts, fontWithWidth } from '@/styles'; - -const Label = styled(ChartXLabel)({ - ...fontWithWidth(fonts.weight.semibold), - fontSize: fonts.size.larger, - fontVariant: ['tabular-nums'], - letterSpacing: fonts.letterSpacing.roundedMedium, - textAlign: 'right', - ...(android ? { marginVertical: -20 } : {}), -}); +import { useTheme } from '@/theme'; const MONTHS = [ lang.t('expanded_state.chart.date.months.month_00'), @@ -31,7 +20,7 @@ const MONTHS = [ lang.t('expanded_state.chart.date.months.month_11'), ]; -function formatDatetime(value, chartTimeDefaultValue) { +function formatDatetime(value: string, chartTimeDefaultValue: string) { 'worklet'; // we have to do it manually due to limitations of reanimated if (value === '') { @@ -82,27 +71,37 @@ function formatDatetime(value, chartTimeDefaultValue) { return res; } -export default function ChartDateLabel({ chartTimeDefaultValue, ratio }) { +export default function ChartDateLabel({ + chartTimeDefaultValue, + ratio, + showPriceChangeStyle, +}: { + chartTimeDefaultValue: string; + ratio: number | undefined; + showPriceChangeStyle: AnimatedStyle; +}) { const { isActive } = useChartData(); - const sharedRatio = useRatio('ChartDataLabel'); + const sharedRatio = useRatio(); const { colors } = useTheme(); const textStyle = useAnimatedStyle(() => { const realRatio = isActive.value ? sharedRatio.value : ratio; return { - color: realRatio === 1 ? colors.blueGreyDark : realRatio < 1 ? colors.red : colors.green, + color: realRatio !== undefined ? (realRatio === 1 ? colors.blueGreyDark : realRatio < 1 ? colors.red : colors.green) : 'transparent', }; - }, [ratio]); + }); + + const formatWorklet = useCallback( + (value: string) => { + 'worklet'; + return formatDatetime(value, chartTimeDefaultValue); + }, + [chartTimeDefaultValue] + ); return ( - - + + + ); } diff --git a/src/components/expanded-state/chart/chart-data-labels/ChartHeaderSubtitle.js b/src/components/expanded-state/chart/chart-data-labels/ChartHeaderSubtitle.js deleted file mode 100644 index 39b3b7dade5..00000000000 --- a/src/components/expanded-state/chart/chart-data-labels/ChartHeaderSubtitle.js +++ /dev/null @@ -1,17 +0,0 @@ -import { TruncatedText } from '../../../text'; -import styled from '@/styled-thing'; - -const ChartHeaderSubtitle = styled(TruncatedText).attrs( - ({ theme: { colors }, color = colors.alpha(colors.blueGreyDark, 0.8), letterSpacing = 'roundedMedium', testID, weight = 'bold' }) => ({ - color, - letterSpacing, - size: 'larger', - testID, - weight, - }) -)({ - flex: 1, - ...(android ? { marginLeft: 9, marginVertical: -10 } : {}), -}); - -export default ChartHeaderSubtitle; diff --git a/src/components/expanded-state/chart/chart-data-labels/ChartHeaderTitle.js b/src/components/expanded-state/chart/chart-data-labels/ChartHeaderTitle.tsx similarity index 100% rename from src/components/expanded-state/chart/chart-data-labels/ChartHeaderTitle.js rename to src/components/expanded-state/chart/chart-data-labels/ChartHeaderTitle.tsx diff --git a/src/components/expanded-state/chart/chart-data-labels/ChartPercentChangeLabel.js b/src/components/expanded-state/chart/chart-data-labels/ChartPercentChangeLabel.js deleted file mode 100644 index d7a15bf0620..00000000000 --- a/src/components/expanded-state/chart/chart-data-labels/ChartPercentChangeLabel.js +++ /dev/null @@ -1,83 +0,0 @@ -import React, { useMemo } from 'react'; -import { TextInput } from 'react-native'; -import Animated, { useAnimatedStyle } from 'react-native-reanimated'; -import { RowWithMargins } from '../../../layout'; -import ChartChangeDirectionArrow from './ChartChangeDirectionArrow'; -import { useRatio } from './useRatio'; -import { useChartData } from '@/react-native-animated-charts/src'; -import styled from '@/styled-thing'; -import { fonts, fontWithWidth } from '@/styles'; - -Animated.addWhitelistedNativeProps({ color: true }); - -const AnimatedTextInput = Animated.createAnimatedComponent(TextInput); - -const PercentLabel = styled(AnimatedTextInput)({ - ...fontWithWidth(fonts.weight.bold), - backgroundColor: ({ theme: { colors } }) => colors.transparent, - fontSize: fonts.size.big, - fontVariant: ['tabular-nums'], - letterSpacing: fonts.letterSpacing.roundedTightest, - textAlign: 'right', - ...(android && { marginVertical: -19 }), -}); - -function formatNumber(num) { - 'worklet'; - const first = num.split('.'); - const digits = first[0].split('').reverse(); - const newDigits = []; - for (let i = 0; i < digits.length; i++) { - newDigits.push(digits[i]); - if ((i + 1) % 3 === 0 && i !== digits.length - 1) { - newDigits.push(','); - } - } - return newDigits.reverse().join('') + '.' + first[1]; -} - -const format = (originalY, data, latestChange) => { - 'worklet'; - const firstValue = data?.points?.[0]?.y; - const lastValue = data?.points?.[data.points.length - 1]?.y; - - return firstValue === Number(firstValue) && firstValue - ? (() => { - const value = - originalY?.value === lastValue || !originalY?.value ? latestChange : ((originalY.value || lastValue) / firstValue) * 100 - 100; - - return (android ? '' : value > 0 ? '↑' : value < 0 ? '↓' : '') + ' ' + formatNumber(Math.abs(value).toFixed(2)) + '%'; - })() - : ''; -}; - -export default function ChartPercentChangeLabel({ ratio, latestChange }) { - const { originalY, data, isActive } = useChartData(); - const { colors } = useTheme(); - - // we don't need to format on latestChange changes - const defaultValue = useMemo(() => format(originalY, data, latestChange), [originalY, data, latestChange]); - - const textProps = useAnimatedStyle( - () => ({ - text: isActive.value ? format(originalY, data, latestChange) : defaultValue, - }), - [originalY, data, latestChange, isActive] - ); - - const sharedRatio = useRatio(); - - const textStyle = useAnimatedStyle(() => { - const realRatio = isActive.value ? sharedRatio.value : ratio; - return { - color: realRatio === 1 ? colors.blueGreyDark : realRatio < 1 ? colors.red : colors.green, - }; - }, [ratio]); - - return ( - - {android ? : null} - - - ); -} diff --git a/src/components/expanded-state/chart/chart-data-labels/ChartPercentChangeLabel.tsx b/src/components/expanded-state/chart/chart-data-labels/ChartPercentChangeLabel.tsx new file mode 100644 index 00000000000..2dca8a3670b --- /dev/null +++ b/src/components/expanded-state/chart/chart-data-labels/ChartPercentChangeLabel.tsx @@ -0,0 +1,72 @@ +import React, { memo } from 'react'; +import { DerivedValue, SharedValue, useAnimatedStyle, useDerivedValue } from 'react-native-reanimated'; +import { AnimatedText } from '@/design-system'; +import { IS_ANDROID } from '@/env'; +import { useChartData } from '@/react-native-animated-charts/src'; +import { useTheme } from '@/theme'; +import { useRatio } from './useRatio'; +import { DataType } from '@/react-native-animated-charts/src/helpers/ChartContext'; + +function formatNumber(num: string) { + 'worklet'; + const first = num.split('.'); + const digits = first[0].split('').reverse(); + const newDigits = []; + for (let i = 0; i < digits.length; i++) { + newDigits.push(digits[i]); + if ((i + 1) % 3 === 0 && i !== digits.length - 1) { + newDigits.push(','); + } + } + return newDigits.reverse().join('') + '.' + first[1]; +} + +const formatWorklet = (originalY: SharedValue, data: DataType, latestChange: number | undefined) => { + 'worklet'; + const firstValue = data?.points?.[0]?.y; + const lastValue = data?.points?.[data.points.length - 1]?.y; + + return firstValue === Number(firstValue) && firstValue + ? (() => { + const originalYNumber = Number(originalY?.value); + const value = + originalYNumber === lastValue || !originalYNumber ? latestChange ?? 0 : ((originalYNumber || lastValue) / firstValue) * 100 - 100; + + return (IS_ANDROID ? '' : value > 0 ? '↑' : value < 0 ? '↓' : '') + ' ' + formatNumber(Math.abs(value).toFixed(2)) + '%'; + })() + : ''; +}; + +export default memo(function ChartPercentChangeLabel({ + latestChange, + ratio, +}: { + latestChange: DerivedValue; + ratio: number | undefined; +}) { + const { originalY, data, isActive } = useChartData(); + const { colors } = useTheme(); + + const sharedRatio = useRatio(); + const text = useDerivedValue(() => formatWorklet(originalY, data, latestChange.value)); + + const textStyle = useAnimatedStyle(() => { + const realRatio = isActive.value ? sharedRatio.value : ratio; + return { + color: realRatio !== undefined ? (realRatio === 1 ? colors.blueGreyDark : realRatio < 1 ? colors.red : colors.green) : 'transparent', + }; + }); + + return ( + + {text} + + ); +}); diff --git a/src/components/expanded-state/chart/chart-data-labels/ChartPriceLabel.js b/src/components/expanded-state/chart/chart-data-labels/ChartPriceLabel.js deleted file mode 100644 index fa17f4152bf..00000000000 --- a/src/components/expanded-state/chart/chart-data-labels/ChartPriceLabel.js +++ /dev/null @@ -1,97 +0,0 @@ -import React from 'react'; -import { PixelRatio, Text } from 'react-native'; -import { useWorkletCallback } from 'react-native-reanimated'; -import { Row } from '../../../layout'; -import ChartHeaderTitle from './ChartHeaderTitle'; -import { ChartYLabel } from '@/react-native-animated-charts/src'; -import { NativeCurrencyKeys } from '@/entities'; -import { useAccountSettings } from '@/hooks'; -import { supportedNativeCurrencies } from '@/references'; -import styled from '@/styled-thing'; -import { fonts, fontWithWidth } from '@/styles'; - -const ChartPriceRow = styled(Row)({}); - -const Label = styled(ChartYLabel)({ - color: ({ theme: { colors } }) => colors.dark, - ...fontWithWidth(fonts.weight.heavy), - fontSize: fonts.size.big, - fontVariant: ({ tabularNums }) => (tabularNums ? ['tabular-nums'] : []), - letterSpacing: fonts.letterSpacing.roundedTight, - ...(android - ? { - marginBottom: -30, - marginTop: -30, - } - : {}), -}); - -const AndroidCurrencySymbolLabel = styled(ChartYLabel)({ - color: ({ theme: { colors } }) => colors.dark, - ...fontWithWidth(fonts.weight.heavy), - fontSize: fonts.size.big, - letterSpacing: fonts.letterSpacing.roundedTight, - - ...(android - ? { - marginBottom: -30, - marginTop: -30, - } - : {}), - - height: 69, - left: 5.5, - marginRight: 3, - top: PixelRatio.get() <= 2.625 ? 22 : 23, -}); - -export function formatNative(value, defaultPriceValue, nativeSelected) { - 'worklet'; - if (!value) { - return defaultPriceValue || ''; - } - if (value === 'undefined') { - return nativeSelected?.alignment === 'left' ? `${nativeSelected?.symbol}0.00` : `0.00 ${nativeSelected?.symbol}`; - } - const decimals = Number(value) < 1 ? Math.min(8, value.toString().slice(2).slice('').search(/[^0]/g) + 3) : 2; - - let res = `${Number(value).toFixed(decimals).toLocaleString('en-US', { - currency: NativeCurrencyKeys.USD, - })}`; - res = nativeSelected?.alignment === 'left' ? `${nativeSelected?.symbol}${res}` : `${res} ${nativeSelected?.symbol}`; - const vals = res.split('.'); - if (vals.length === 2) { - return vals[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',') + '.' + vals[1]; - } - return res; -} - -export default function ChartPriceLabel({ defaultValue, isNoPriceData, priceValue, tabularNums }) { - const { nativeCurrency } = useAccountSettings(); - const nativeSelected = supportedNativeCurrencies?.[nativeCurrency]; - - const format = useWorkletCallback( - value => { - 'worklet'; - const formatted = formatNative(value, priceValue, nativeSelected); - if (android) { - return formatted.replace(/[^\d.,-]/g, ''); - } - return formatted; - }, - [nativeSelected, priceValue] - ); - - return isNoPriceData ? ( - {defaultValue} - ) : ( - - {android && ( - - {nativeSelected?.symbol} - - )} - - ); -} diff --git a/src/components/expanded-state/chart/chart-data-labels/ChartPriceLabel.tsx b/src/components/expanded-state/chart/chart-data-labels/ChartPriceLabel.tsx new file mode 100644 index 00000000000..4b9f7264c5d --- /dev/null +++ b/src/components/expanded-state/chart/chart-data-labels/ChartPriceLabel.tsx @@ -0,0 +1,102 @@ +import React, { useCallback } from 'react'; +import { Text } from '@/design-system'; +import { useAccountSettings } from '@/hooks'; +import { ChartYLabel } from '@/react-native-animated-charts/src'; +import { SupportedCurrency, supportedNativeCurrencies } from '@/references'; +import { DEVICE_WIDTH } from '@/utils/deviceUtils'; +import { orderOfMagnitudeWorklet, significantDecimalsWorklet } from '@/safe-math/SafeMath'; + +function calculateDecimalPlaces({ + amount, + minimumDecimals = 0, + maximumDecimals = 6, + precisionAdjustment, +}: { + amount: number | string; + minimumDecimals?: number; + maximumDecimals?: number; + precisionAdjustment?: number; +}): { + minimumDecimalPlaces: number; + maximumDecimalPlaces: number; +} { + 'worklet'; + const orderOfMagnitude = orderOfMagnitudeWorklet(amount); + const significantDecimals = significantDecimalsWorklet(amount); + + let minimumDecimalPlaces = minimumDecimals; + let maximumDecimalPlaces = maximumDecimals; + + const minBasedOnOrderOfMag = orderOfMagnitude > 2 ? 0 : 2; + if (orderOfMagnitude < 1) { + minimumDecimalPlaces = Math.max(minimumDecimals, significantDecimals); + maximumDecimalPlaces = Math.max(minBasedOnOrderOfMag, significantDecimals + 1); + } else { + minimumDecimalPlaces = minimumDecimals; + maximumDecimalPlaces = minBasedOnOrderOfMag; + } + + return { + minimumDecimalPlaces, + maximumDecimalPlaces: Math.max(maximumDecimalPlaces + (precisionAdjustment ?? 0), minimumDecimalPlaces), + }; +} + +export function formatNative(value: string, defaultPriceValue: string | null, nativeSelected: SupportedCurrency[keyof SupportedCurrency]) { + 'worklet'; + if (!value) { + return defaultPriceValue || ''; + } + if (value === 'undefined') { + return nativeSelected?.alignment === 'left' ? `${nativeSelected?.symbol}0.00` : `0.00 ${nativeSelected?.symbol}`; + } + + const { maximumDecimalPlaces: numDecimals } = calculateDecimalPlaces({ + amount: value, + minimumDecimals: 2, + maximumDecimals: 6, + precisionAdjustment: 1, + }); + + let res = `${Number(value).toFixed(numDecimals).toLocaleString()}`; + res = nativeSelected?.alignment === 'left' ? `${nativeSelected?.symbol}${res}` : `${res} ${nativeSelected?.symbol}`; + const vals = res.split('.'); + if (vals.length === 2) { + return vals[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',') + '.' + vals[1]; + } + return res; +} + +export default function ChartPriceLabel({ + defaultValue, + isNoPriceData, + priceValue, +}: { + defaultValue: string; + isNoPriceData: boolean; + priceValue: string; +}) { + const { nativeCurrency } = useAccountSettings(); + const nativeSelected = supportedNativeCurrencies?.[nativeCurrency]; + + const formatWorklet = useCallback( + (value: string) => { + 'worklet'; + return formatNative(value, priceValue, nativeSelected); + }, + [nativeSelected, priceValue] + ); + + return isNoPriceData ? ( + + {defaultValue} + + ) : ( + + ); +} diff --git a/src/components/expanded-state/chart/chart-data-labels/index.js b/src/components/expanded-state/chart/chart-data-labels/index.ts similarity index 67% rename from src/components/expanded-state/chart/chart-data-labels/index.js rename to src/components/expanded-state/chart/chart-data-labels/index.ts index 01c1352e75f..c88ea30b4de 100644 --- a/src/components/expanded-state/chart/chart-data-labels/index.js +++ b/src/components/expanded-state/chart/chart-data-labels/index.ts @@ -1,6 +1,4 @@ export { default as ChartChangeDirectionArrow } from './ChartChangeDirectionArrow'; export { default as ChartDateLabel } from './ChartDateLabel'; -export { default as ChartHeaderSubtitle } from './ChartHeaderSubtitle'; -export { default as ChartHeaderTitle } from './ChartHeaderTitle'; export { default as ChartPercentChangeLabel } from './ChartPercentChangeLabel'; export { default as ChartPriceLabel } from './ChartPriceLabel'; diff --git a/src/components/expanded-state/chart/chart-data-labels/useRatio.js b/src/components/expanded-state/chart/chart-data-labels/useRatio.ts similarity index 77% rename from src/components/expanded-state/chart/chart-data-labels/useRatio.js rename to src/components/expanded-state/chart/chart-data-labels/useRatio.ts index 5bfac43bb55..f6db5e39b98 100644 --- a/src/components/expanded-state/chart/chart-data-labels/useRatio.js +++ b/src/components/expanded-state/chart/chart-data-labels/useRatio.ts @@ -8,6 +8,6 @@ export function useRatio() { const firstValue = data?.points?.[0]?.y; const lastValue = data?.points?.[data.points.length - 1]?.y; - return firstValue === Number(firstValue) ? (originalY.value || lastValue) / firstValue : 1; + return firstValue === Number(firstValue) ? (Number(originalY.value) || lastValue) / firstValue : 1; }, [data]); } diff --git a/src/components/expanded-state/chart/index.js b/src/components/expanded-state/chart/index.ts similarity index 100% rename from src/components/expanded-state/chart/index.js rename to src/components/expanded-state/chart/index.ts diff --git a/src/components/value-chart/Chart.js b/src/components/value-chart/Chart.js index a373c5612c1..9882bf872e6 100644 --- a/src/components/value-chart/Chart.js +++ b/src/components/value-chart/Chart.js @@ -15,6 +15,7 @@ import { useNavigation } from '@/navigation'; import styled from '@/styled-thing'; import { position } from '@/styles'; import { DOG_ADDRESS } from '@/references'; +import { IS_IOS } from '@/env'; export const { width: WIDTH } = Dimensions.get('window'); @@ -43,7 +44,7 @@ const ChartSpinner = styled(ImgixImage).attrs(({ color }) => ({ const Container = styled(Column)({ paddingBottom: 30, - paddingTop: ios ? 0 : 20, + paddingTop: IS_IOS ? 0 : 4, width: '100%', }); @@ -115,6 +116,7 @@ export default function ChartWrapper({ color, fetchingCharts, isPool, + latestChange, updateChartType, showChart, testID, @@ -175,7 +177,15 @@ export default function ChartWrapper({ return ( - + {showChart && ( <> diff --git a/src/components/value-chart/ExtremeLabels.js b/src/components/value-chart/ExtremeLabels.tsx similarity index 58% rename from src/components/value-chart/ExtremeLabels.js rename to src/components/value-chart/ExtremeLabels.tsx index 47b3bc07a78..c4a689039ed 100644 --- a/src/components/value-chart/ExtremeLabels.js +++ b/src/components/value-chart/ExtremeLabels.tsx @@ -1,23 +1,39 @@ import React, { useCallback, useMemo, useState } from 'react'; -import { View } from 'react-native'; +import { LayoutChangeEvent, StyleProp, View, ViewStyle } from 'react-native'; import { formatNative } from '../expanded-state/chart/chart-data-labels/ChartPriceLabel'; import { useChartData } from '@/react-native-animated-charts/src'; import { Text } from '@/design-system'; import { useAccountSettings } from '@/hooks'; import { supportedNativeCurrencies } from '@/references'; +import { useTheme } from '@/theme'; +import { TextSize } from '@/design-system/typography/typeHierarchy'; -function trim(val) { +function trim(val: number) { return Math.min(Math.max(val, 0.05), 0.95); } -const CenteredLabel = ({ position, fontSize = '14px / 19px (Deprecated)', style, width, ...props }) => { +const CenteredLabel = ({ + children, + color, + position, + size = '14px / 19px (Deprecated)', + style, + width, +}: { + children: React.ReactNode; + color: string; + position: number; + size?: TextSize; + style: StyleProp; + width: number; +}) => { const [componentWidth, setWidth] = useState(0); const onLayout = useCallback( ({ nativeEvent: { layout: { width: newWidth }, }, - }) => { + }: LayoutChangeEvent) => { setWidth(newWidth); }, [setWidth] @@ -30,29 +46,30 @@ const CenteredLabel = ({ position, fontSize = '14px / 19px (Deprecated)', style, return ( - - {props.children} + + {children} ); }; -const Labels = ({ color, width, isCard }) => { +const Labels = ({ color, width, isCard }: { color: string; width: number; isCard: boolean }) => { const { nativeCurrency } = useAccountSettings(); const nativeSelected = supportedNativeCurrencies?.[nativeCurrency]; const { greatestX, greatestY, smallestX, smallestY } = useChartData(); const { colors } = useTheme(); - if (!greatestX) { - return null; - } + if (!greatestX || !greatestY || !smallestX || !smallestY) return null; + const positionMin = trim((smallestY.x - smallestX.x) / (greatestX.x - smallestX.x)); const positionMax = trim((greatestY.x - smallestX.x) / (greatestX.x - smallestX.x)); @@ -62,27 +79,22 @@ const Labels = ({ color, width, isCard }) => { - {formatNative(smallestY.y, null, nativeSelected)} + {formatNative(smallestY.y.toString(), null, nativeSelected)} ) : null} {positionMax ? ( - {formatNative(greatestY.y, null, nativeSelected)} + {formatNative(greatestY.y.toString(), null, nativeSelected)} ) : null} diff --git a/src/hooks/charts/useChartDataLabels.ts b/src/hooks/charts/useChartDataLabels.ts index 53ca83089e7..2e6979739c3 100644 --- a/src/hooks/charts/useChartDataLabels.ts +++ b/src/hooks/charts/useChartDataLabels.ts @@ -1,15 +1,37 @@ -import { useCallback, useMemo } from 'react'; +import { useCallback } from 'react'; +import { useDerivedValue } from 'react-native-reanimated'; import ChartTypes, { ChartType } from '@/helpers/chartTypes'; -import { toFixedDecimals } from '@/helpers/utilities'; +import { toFixedWorklet } from '@/safe-math/SafeMath'; +import { AssetApiResponse, AssetMetadata } from '@/__swaps__/types/assets'; import { ChartData } from './useChartInfo'; -const formatPercentChange = (change = 0) => toFixedDecimals(change, 2); +const formatPercentChange = (change = 0) => { + 'worklet'; + return toFixedWorklet(change, 2); +}; -export default function useChartDataLabels({ asset, chartType, points }: { asset: any; chartType: ChartType; points: ChartData[] }) { +type AssetWithPrice = (AssetApiResponse | AssetMetadata) & { + price?: { + relative_change_24h?: number; + relativeChange24h?: number; + value: number; + }; +}; + +export default function useChartDataLabels({ + asset, + chartType, + points, +}: { + asset: AssetWithPrice; + chartType: ChartType; + points: ChartData[]; +}) { const latestPrice = asset?.price?.value; const getPercentChangeForPrice = useCallback( (startPrice: number) => { + 'worklet'; const endPrice = points[points.length - 1]?.y; if (!endPrice) return; const percent = ((endPrice - startPrice) / startPrice) * 100; @@ -18,13 +40,14 @@ export default function useChartDataLabels({ asset, chartType, points }: { asset [points] ); - const latestChange = useMemo( - () => - !points || chartType === ChartTypes.day - ? formatPercentChange(asset?.price?.relative_change_24h || asset?.price?.relativeChange24h) - : getPercentChangeForPrice(points[0]?.y ?? 0), - [asset, chartType, getPercentChangeForPrice, points] - ); + const latestChange = useDerivedValue(() => { + if (!points || chartType === ChartTypes.day) { + // Handle both AssetApiResponse and AssetMetadata price change fields + const change = asset?.price?.relative_change_24h ?? asset?.price?.relativeChange24h ?? 0; + return formatPercentChange(change); + } + return getPercentChangeForPrice(points[0]?.y ?? 0); + }); return { latestChange, diff --git a/src/hooks/charts/useChartThrottledPoints.ts b/src/hooks/charts/useChartThrottledPoints.ts index 792f9f85805..eb842b57234 100644 --- a/src/hooks/charts/useChartThrottledPoints.ts +++ b/src/hooks/charts/useChartThrottledPoints.ts @@ -33,11 +33,11 @@ const traverseData = (prev: { nativePoints: ChartData[]; points: ChartData[] }, }; function useJumpingForm( - isLong: any, - heightWithChart: any, - heightWithoutChart: any, - shortHeightWithChart: any, - shortHeightWithoutChart: any + isLong: boolean, + heightWithChart?: number, + heightWithoutChart?: number, + shortHeightWithChart?: number, + shortHeightWithoutChart?: number ) { const { setOptions } = useNavigation(); @@ -74,7 +74,15 @@ export default function useChartThrottledPoints({ uniBalance = true, shortHeightWithChart, shortHeightWithoutChart, -}: any) { +}: { + asset: any; + heightWithChart?: number; + heightWithoutChart?: number; + isPool?: boolean; + uniBalance?: boolean; + shortHeightWithChart?: number; + shortHeightWithoutChart?: number; +}) { const { nativeCurrency } = useAccountSettings(); let assetForColor = asset; @@ -84,8 +92,6 @@ export default function useChartThrottledPoints({ const color = useColorForAsset(assetForColor); - const [isFetchingInitially, setIsFetchingInitially] = useState(true); - const { params } = useRoute<{ key: string; name: string; @@ -114,26 +120,31 @@ export default function useChartThrottledPoints({ points: throttledPoints.points ?? [], }); - useEffect(() => { - if (!fetchingCharts) { - setIsFetchingInitially(false); - } - }, [fetchingCharts]); - // Only show the chart if we have chart data, or if chart data is still loading const showChart = useMemo( () => (nativeCurrency !== 'ETH' || (asset?.mainnet_address !== ETH_ADDRESS && asset?.address !== ETH_ADDRESS)) && - (throttledPoints?.points.length > 5 || throttledPoints?.points.length > 5 || (fetchingCharts && !isFetchingInitially)), - [asset?.address, asset?.mainnet_address, fetchingCharts, isFetchingInitially, nativeCurrency, throttledPoints?.points.length] + (throttledPoints?.points.length > 5 || + !!chart.length || + (fetchingCharts && (!!asset?.native?.change || (asset?.native?.price?.amount && asset?.native?.price?.amount !== '0')))), + [ + asset?.address, + asset?.mainnet_address, + asset?.native?.change, + asset?.native?.price?.amount, + chart.length, + fetchingCharts, + nativeCurrency, + throttledPoints?.points.length, + ] ); useJumpingForm( showChart, - heightWithChart - (uniBalance ? 0 : UniBalanceHeightDifference), - heightWithoutChart - (uniBalance ? 0 : UniBalanceHeightDifference), - shortHeightWithChart - (uniBalance ? 0 : UniBalanceHeightDifference), - shortHeightWithoutChart - (uniBalance ? 0 : UniBalanceHeightDifference) + heightWithChart ? heightWithChart - (uniBalance ? 0 : UniBalanceHeightDifference) : undefined, + heightWithoutChart ? heightWithoutChart - (uniBalance ? 0 : UniBalanceHeightDifference) : undefined, + shortHeightWithChart ? shortHeightWithChart - (uniBalance ? 0 : UniBalanceHeightDifference) : undefined, + shortHeightWithoutChart ? shortHeightWithoutChart - (uniBalance ? 0 : UniBalanceHeightDifference) : undefined ); const [throttledData, setThrottledData] = useState({ diff --git a/src/navigation/config.tsx b/src/navigation/config.tsx index 9097c84e1d6..15ea676d5d9 100644 --- a/src/navigation/config.tsx +++ b/src/navigation/config.tsx @@ -527,6 +527,8 @@ export const expandedAssetSheetConfig: PartialNavigatorConfigOptions = { ...buildCoolModalConfig({ ...params, scrollEnabled: true, + springDamping: 1, + transitionDuration: 0.28, }), }), }; @@ -536,6 +538,8 @@ export const expandedAssetSheetConfigWithLimit: PartialNavigatorConfigOptions = ...buildCoolModalConfig({ ...params, scrollEnabled: true, + springDamping: 1, + transitionDuration: 0.28, }), limitActiveModals: true, }), diff --git a/src/react-native-animated-charts/src/charts/linear/ChartLabels.tsx b/src/react-native-animated-charts/src/charts/linear/ChartLabels.tsx index 135bec74a2e..56be057857c 100644 --- a/src/react-native-animated-charts/src/charts/linear/ChartLabels.tsx +++ b/src/react-native-animated-charts/src/charts/linear/ChartLabels.tsx @@ -1,37 +1,27 @@ -import React, { useMemo } from 'react'; -import { TextInput, TextInputProps } from 'react-native'; -import Animated, { useAnimatedProps } from 'react-native-reanimated'; +import React, { memo } from 'react'; +import { useDerivedValue } from 'react-native-reanimated'; +import { AnimatedText, AnimatedTextProps } from '@/design-system'; import { useChartData } from '../../helpers/useChartData'; -const AnimatedTextInput = Animated.createAnimatedComponent(TextInput); - -interface ChartLabelProps extends TextInputProps { - format: (value: string) => string; +interface ChartLabelProps extends Partial { + formatWorklet: (value: string) => string; } const ChartLabelFactory = (fieldName: 'originalX' | 'originalY') => { - const ChartLabel = React.memo(({ format, ...props }: ChartLabelProps) => { - const { isActive, data, ...chartData } = useChartData(); - const val = chartData[fieldName]; - - // we need to recreate defaultValue on data change - // eslint-disable-next-line react-hooks/exhaustive-deps - const defaultValue = useMemo(() => format?.(val.value) ?? val.value, [format, val, data]); - - const textProps = useAnimatedProps( - () => ({ - text: isActive.value ? format?.(val.value) ?? val.value : defaultValue, - value: isActive.value ? format?.(val.value) ?? val.value : defaultValue, - }), - [data, defaultValue, isActive] + return memo(function ChartLabel({ formatWorklet, ...props }: ChartLabelProps) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { isActive: _, data: __, ...chartData } = useChartData(); + + const sharedValue = chartData[fieldName]; + const text = useDerivedValue(() => formatWorklet?.(sharedValue.value) ?? sharedValue.value); + + return ( + // eslint-disable-next-line react/jsx-props-no-spreading + + {text} + ); - - return ; }); - - ChartLabel.displayName = 'ChartLabel'; - - return ChartLabel; }; export const ChartYLabel = ChartLabelFactory('originalY'); diff --git a/src/react-native-animated-charts/src/charts/linear/ChartPath.tsx b/src/react-native-animated-charts/src/charts/linear/ChartPath.tsx index f09b5bad84f..04b7a643a95 100644 --- a/src/react-native-animated-charts/src/charts/linear/ChartPath.tsx +++ b/src/react-native-animated-charts/src/charts/linear/ChartPath.tsx @@ -1,28 +1,28 @@ -import React, { useEffect } from 'react'; -import { Platform, View, ViewProps } from 'react-native'; +import React, { useCallback, useEffect } from 'react'; +import { View, ViewProps } from 'react-native'; import { LongPressGestureHandler, LongPressGestureHandlerGestureEvent, LongPressGestureHandlerProperties, } from 'react-native-gesture-handler'; -import ReactNativeHapticFeedback from 'react-native-haptic-feedback'; import Animated, { + FadeIn, cancelAnimation, - runOnJS, runOnUI, useAnimatedGestureHandler, useAnimatedProps, useAnimatedReaction, - useAnimatedStyle, + useDerivedValue, useSharedValue, - useWorkletCallback, withDelay, WithSpringConfig, withTiming, WithTimingConfig, } from 'react-native-reanimated'; import { getYForX } from 'react-native-redash'; -import Svg, { Path, PathProps } from 'react-native-svg'; +import Svg, { NumberProp, Path, PathProps } from 'react-native-svg'; +import { triggerHaptics } from 'react-native-turbo-haptics'; +import { IS_ANDROID, IS_IOS } from '@/env'; import { ChartData, PathData } from '../../helpers/ChartContext'; import { requireOnWorklet, useWorkletValue } from '../../helpers/requireOnWorklet'; import { useChartData } from '../../helpers/useChartData'; @@ -59,11 +59,6 @@ function least(length: number, compare: (value: number) => number) { } } -function impactHeavy() { - 'worklet'; - runOnJS(() => ReactNativeHapticFeedback.trigger('impactHeavy')); -} - const timingFeedbackDefaultConfig = { duration: 80, }; @@ -136,22 +131,27 @@ const ChartPathInner = React.memo( const translationX = useSharedValue(null); const translationY = useSharedValue(null); - const setOriginData = useWorkletCallback((path: PathData, index?: number) => { - if (!path.data.length) { - return; - } + const setOriginData = useCallback( + (path: PathData, index?: number) => { + 'worklet'; + if (!path.data.length) { + return; + } - if (typeof index === 'undefined') { - originalX.value = ''; - originalY.value = ''; - return; - } + if (typeof index === 'undefined') { + originalX.value = ''; + originalY.value = ''; + return; + } - originalX.value = path.data[index].x.toString(); - originalY.value = path.data[index].y.toString(); - }, []); + originalX.value = path.data[index].x.toString(); + originalY.value = path.data[index].y.toString(); + }, + [originalX, originalY] + ); - const resetGestureState = useWorkletCallback(() => { + const resetGestureState = useCallback(() => { + 'worklet'; originalX.value = ''; originalY.value = ''; positionY.value = -1; @@ -159,11 +159,10 @@ const ChartPathInner = React.memo( pathOpacity.value = withTiming(1, timingFeedbackConfig || timingFeedbackDefaultConfig); translationX.value = null; translationY.value = null; - }, []); + }, [originalX, originalY, positionY, isActive, pathOpacity, translationX, translationY, timingFeedbackConfig]); useEffect(() => { runOnUI(() => { - 'worklet'; if (currentPath) { setOriginData(currentPath); } @@ -186,7 +185,7 @@ const ChartPathInner = React.memo( progress.value = 0; - progress.value = withDelay(Platform.OS === 'ios' ? 0 : 100, withTiming(1, timingAnimationConfig || timingAnimationDefaultConfig)); + progress.value = withDelay(IS_IOS ? 0 : 100, withTiming(1, timingAnimationConfig || timingAnimationDefaultConfig)); } else { interpolatorWorklet().value = undefined; progress.value = 1; @@ -264,7 +263,7 @@ const ChartPathInner = React.memo( props.strokeWidth = pathOpacity.value * (Number(strokeWidth) - Number(selectedStrokeWidth)) + Number(selectedStrokeWidth); - if (Platform.OS === 'ios') { + if (IS_IOS) { props.style = { opacity: pathOpacity.value * (1 - selectedOpacity) + selectedOpacity, }; @@ -281,9 +280,7 @@ const ChartPathInner = React.memo( pathOpacity.value = withTiming(0, timingFeedbackConfig || timingFeedbackDefaultConfig); - if (hapticsEnabled) { - impactHeavy(); - } + if (hapticsEnabled) triggerHaptics('soft'); } state.value = event.state; @@ -298,9 +295,7 @@ const ChartPathInner = React.memo( state.value = event.state; resetGestureState(); - if (hapticsEnabled) { - impactHeavy(); - } + if (hapticsEnabled) triggerHaptics('soft'); }, onFail: event => { state.value = event.state; @@ -310,34 +305,30 @@ const ChartPathInner = React.memo( // WARNING: the following code does not run on using iOS, but it does on Android. // I use the same code from onActive // platform is for safety - if (Platform.OS === 'android') { + if (IS_ANDROID) { state.value = event.state; isActive.value = true; pathOpacity.value = withTiming(0, timingFeedbackConfig || timingFeedbackDefaultConfig); - if (hapticsEnabled) { - impactHeavy(); - } + if (hapticsEnabled) triggerHaptics('soft'); } }, }, [width, height, hapticsEnabled, hitSlop, timingFeedbackConfig] ); - const pathAnimatedStyles = useAnimatedStyle(() => { - return { - opacity: pathOpacity.value * (1 - selectedOpacity) + selectedOpacity, - }; + const opacityProp = useDerivedValue(() => { + return pathOpacity.value * (1 - selectedOpacity) + selectedOpacity; }); return ( - // @ts-ignore @@ -350,11 +341,11 @@ const ChartPathInner = React.memo( > @@ -384,45 +375,43 @@ export const ChartPath = React.memo( const { positionX, positionY, originalX, originalY, state, isActive, progress, pathOpacity, currentPath, previousPath } = useChartData(); - let renderPath = null; - - // this is workaround to avoid unnecessary rerenders of the path component - // and sometimes blank SvgPath the currentPath's path string is empty - // due to some broken logic in the Rainbow app - if (currentPath?.path) { - renderPath = ( - - ); - } - - return {renderPath}; + return ( + + {currentPath?.path ? ( + + + + ) : null} + + ); } ); diff --git a/src/react-native-animated-charts/src/charts/linear/ChartPathProvider.tsx b/src/react-native-animated-charts/src/charts/linear/ChartPathProvider.tsx index bc4659cba17..80d797dcb5a 100644 --- a/src/react-native-animated-charts/src/charts/linear/ChartPathProvider.tsx +++ b/src/react-native-animated-charts/src/charts/linear/ChartPathProvider.tsx @@ -19,7 +19,7 @@ interface ChartPathProviderProps { children: React.ReactNode; } -function getCurveType(curveType: keyof typeof CurveType) { +function getCurveType(curveType: keyof typeof CurveType | undefined) { switch (curveType) { case CurveType.basis: return shape.curveBasis; @@ -97,7 +97,7 @@ function createPath({ data, width, height, yRange }: CallbackType): PathData { .line() .x(item => scaleX(item.x)) .y(item => scaleY(item.y)) - .curve(getCurveType(data.curve!))(data.points); + .curve(getCurveType(data.curve))(data.points); if (path === null) { return { @@ -161,7 +161,7 @@ export const ChartPathProvider = React.memo(({ children, // because we do have initial data in the paths // we wait until we receive new stuff in deps if (initialized.current) { - setPaths(([_, curr]) => [curr, data.points.length ? createPath({ data, height, width, yRange }) : null]); + setPaths(([, curr]) => [curr, data.points.length ? createPath({ data, height, width, yRange }) : null]); } else { // componentDidMount hack initialized.current = true; diff --git a/src/react-native-animated-charts/src/helpers/ChartContext.ts b/src/react-native-animated-charts/src/helpers/ChartContext.ts index 118c8d60feb..cb30230808a 100644 --- a/src/react-native-animated-charts/src/helpers/ChartContext.ts +++ b/src/react-native-animated-charts/src/helpers/ChartContext.ts @@ -46,7 +46,9 @@ export interface PathScales { scaleY: (value: number) => number; } -export interface ChartData { +type WithPathData = Pick; + +export type ChartData = { data: DataType; width: number; height: number; @@ -61,6 +63,6 @@ export interface ChartData { positionY: SharedValue; previousPath: PathData | null; currentPath: PathData | null; -} +} & WithPathData; export const ChartContext = React.createContext(null); diff --git a/src/react-native-animated-charts/src/helpers/useChartData.ts b/src/react-native-animated-charts/src/helpers/useChartData.ts index 51178939638..eed627cf64d 100644 --- a/src/react-native-animated-charts/src/helpers/useChartData.ts +++ b/src/react-native-animated-charts/src/helpers/useChartData.ts @@ -2,7 +2,7 @@ import { useContext } from 'react'; import { ChartContext, ChartData } from './ChartContext'; export function useChartData(): ChartData { - const ctx = useContext(ChartContext)!; + const ctx = useContext(ChartContext); if (ctx === null) { throw new Error('Cannot resolve Chart context. Did you forget to use ChartPathProvider?'); diff --git a/src/react-native-animated-charts/src/helpers/useReactiveSharedValue.js b/src/react-native-animated-charts/src/helpers/useReactiveSharedValue.ts similarity index 69% rename from src/react-native-animated-charts/src/helpers/useReactiveSharedValue.js rename to src/react-native-animated-charts/src/helpers/useReactiveSharedValue.ts index bc647ba2113..d7d61aebb3a 100644 --- a/src/react-native-animated-charts/src/helpers/useReactiveSharedValue.js +++ b/src/react-native-animated-charts/src/helpers/useReactiveSharedValue.ts @@ -1,8 +1,8 @@ import { useEffect } from 'react'; import { useSharedValue } from 'react-native-reanimated'; -export default function useReactiveSharedValue(prop, name) { - const sharedValue = useSharedValue(null, name); +export default function useReactiveSharedValue(prop: T) { + const sharedValue = useSharedValue(null); if (sharedValue.value === null) { sharedValue.value = prop; } diff --git a/src/react-native-animated-charts/tsconfig.json b/src/react-native-animated-charts/tsconfig.json new file mode 100644 index 00000000000..99c9069c5e4 --- /dev/null +++ b/src/react-native-animated-charts/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "composite": true, + "rootDir": "." + } +}