diff --git a/jestSetup.js b/jestSetup.js index c6254c92090..d56ef11dc76 100644 --- a/jestSetup.js +++ b/jestSetup.js @@ -254,3 +254,8 @@ jest.mock('react-native-device-info', () => { getVersion: jest.fn() } }) + +jest.mock('react-native-reorderable-list', () => ({ + ...jest.requireActual('react-native-reorderable-list'), + useReorderableDrag: () => jest.fn() +})) diff --git a/package.json b/package.json index 398dab4006b..5c764df1245 100644 --- a/package.json +++ b/package.json @@ -123,7 +123,6 @@ "react-native-contacts": "^7.0.4", "react-native-custom-tabs": "https://github.com/adminphoeniixx/react-native-custom-tabs#develop", "react-native-device-info": "^13.2.0", - "react-native-draggable-flatlist": "^4.0.1", "react-native-email-link": "^1.14.5", "react-native-fast-image": "^8.5.11", "react-native-fast-shadow": "^0.1.0", @@ -145,6 +144,7 @@ "react-native-permissions": "^4.1.5", "react-native-piratechain": "^0.5.4", "react-native-reanimated": "^3.14.0", + "react-native-reorderable-list": "^0.5.0", "react-native-safari-view": "^2.1.0", "react-native-safe-area-context": "^4.10.1", "react-native-screens": "^3.31.1", diff --git a/src/__tests__/components/WalletListSortableRow.test.tsx b/src/__tests__/components/WalletListSortableRow.test.tsx index 4e29da2f1ad..c233dcea666 100644 --- a/src/__tests__/components/WalletListSortableRow.test.tsx +++ b/src/__tests__/components/WalletListSortableRow.test.tsx @@ -10,7 +10,7 @@ describe('WalletListSortableRow', () => { it('should render with loading wallet', () => { const renderer = TestRenderer.create( - {}} /> + ) expect(renderer.toJSON()).toMatchSnapshot() @@ -48,7 +48,7 @@ describe('WalletListSortableRow', () => { const renderer = TestRenderer.create( - {}} /> + ) expect(renderer.toJSON()).toMatchSnapshot() diff --git a/src/__tests__/components/__snapshots__/WalletListSortableRow.test.tsx.snap b/src/__tests__/components/__snapshots__/WalletListSortableRow.test.tsx.snap index 40dc0764af4..c62822efe4d 100644 --- a/src/__tests__/components/__snapshots__/WalletListSortableRow.test.tsx.snap +++ b/src/__tests__/components/__snapshots__/WalletListSortableRow.test.tsx.snap @@ -2,48 +2,54 @@ exports[`WalletListSortableRow should render with fake wallet 1`] = ` - + @@ -79,199 +85,199 @@ exports[`WalletListSortableRow should render with fake wallet 1`] = `  + + - - - + } + /> + + - - - FAKE - - - 0 - - - + - - Test wallet - - - $0.00 - - + 0 + + + + + Test wallet + + + $0.00 + diff --git a/src/components/themed/WalletListSortable.tsx b/src/components/themed/WalletListSortable.tsx index 735a87759be..7addec38a04 100644 --- a/src/components/themed/WalletListSortable.tsx +++ b/src/components/themed/WalletListSortable.tsx @@ -1,19 +1,16 @@ import { EdgeWalletStates } from 'edge-core-js' import * as React from 'react' -import DraggableFlatList, { DragEndParams, RenderItemParams, ScaleDecorator } from 'react-native-draggable-flatlist' -import Animated from 'react-native-reanimated' +import ReorderableList, { ReorderableListItem } from 'react-native-reorderable-list' import { SCROLL_INDICATOR_INSET_FIX } from '../../constants/constantSettings' import { useHandler } from '../../hooks/useHandler' import { useWatch } from '../../hooks/useWatch' -import { useSceneScrollHandler } from '../../state/SceneScrollState' +import { useSceneScrollWorkletHandler } from '../../state/SceneScrollState' import { useSelector } from '../../types/reactRedux' import { InsetStyle } from '../common/SceneWrapper' import { showError } from '../services/AirshipInstance' import { WalletListSortableRow } from './WalletListSortableRow' -const AnimatedDraggableFlatList = Animated.createAnimatedComponent(DraggableFlatList) - interface Props { insetStyle?: InsetStyle } @@ -23,39 +20,50 @@ interface Props { */ export function WalletListSortable(props: Props) { const { insetStyle } = props + // Subscribe to account state: const account = useSelector(state => state.core.account) const currencyWallets = useWatch(account, 'currencyWallets') const [walletOrder, setWalletOrder] = React.useState(account.activeWalletIds) - const handleDragEnd = useHandler((params: DragEndParams) => setWalletOrder(params.data)) - const keyExtractor = useHandler((walletId: string) => walletId) - const renderItem = useHandler((params: RenderItemParams) => ( - - - - )) - const handleScroll = useSceneScrollHandler() + const handleReorder = useHandler(({ from, to }: { from: number; to: number }) => { + // Reorder the walletOrder array + const newOrder = [...walletOrder] + newOrder.splice(to, 0, newOrder.splice(from, 1)[0]) + setWalletOrder(newOrder) - React.useEffect(() => () => { + // Update the wallet sort order in the account const keyStates: EdgeWalletStates = {} - for (let i = 0; i < walletOrder.length; ++i) { - const walletId = walletOrder[i] + for (let i = 0; i < newOrder.length; ++i) { + const walletId = newOrder[i] keyStates[walletId] = { sortIndex: i } } account.changeWalletStates(keyStates).catch(error => showError(error)) }) + const keyExtractor = React.useCallback( + (item: string) => item, + // eslint-disable-next-line react-hooks/exhaustive-deps + [walletOrder] + ) + + const renderItem = useHandler(item => { + return ( + + + + ) + }) + + const handleScroll = useSceneScrollWorkletHandler() + return ( - void } function WalletListSortableRowComponent(props: Props) { - const { wallet, onDrag } = props + const { wallet } = props + + const handleDrag = useReorderableDrag() const theme = useTheme() const styles = getStyles(theme) @@ -36,7 +38,7 @@ function WalletListSortableRowComponent(props: Props) { if (wallet == null || exchangeDenomination == null) { return ( - + @@ -62,26 +64,26 @@ function WalletListSortableRowComponent(props: Props) { const fiatBalanceString = showBalance ? formatNumber(fiatBalanceFormat, { toFixed: FIAT_PRECISION }) : '' return ( - - + + - - + + + + + + + {currencyCode} + {finalCryptoAmountString} - - - {currencyCode} - {finalCryptoAmountString} - - - {name} - {fiatBalanceSymbol + fiatBalanceString} - + + {name} + {fiatBalanceSymbol + fiatBalanceString} - + ) } @@ -89,6 +91,10 @@ const getStyles = cacheStyles((theme: Theme) => ({ container: { paddingHorizontal: theme.rem(1) }, + handleContainer: { + margin: -theme.rem(0.5), + padding: theme.rem(0.5) + }, rowContainer: { flexDirection: 'row', justifyContent: 'center', diff --git a/src/state/SceneScrollState.tsx b/src/state/SceneScrollState.tsx index 836c2944ac8..adb2b7109de 100644 --- a/src/state/SceneScrollState.tsx +++ b/src/state/SceneScrollState.tsx @@ -1,7 +1,7 @@ import { useIsFocused } from '@react-navigation/native' import { useMemo } from 'react' import { NativeScrollEvent, NativeSyntheticEvent } from 'react-native' -import { SharedValue, useAnimatedReaction, useAnimatedScrollHandler, useSharedValue } from 'react-native-reanimated' +import { SharedValue, useAnimatedReaction, useAnimatedScrollHandler, useSharedValue, useWorkletCallback } from 'react-native-reanimated' import { createStateProvider } from './createStateProvider' @@ -113,3 +113,39 @@ export const useSceneScrollHandler = (): SceneScrollHandler => { return handler } + +/** Like `useSceneScrollHandler`, but specifically for worklets that need a + * `(event: NativeScrollEvent) => void` type prop */ +export const useSceneScrollWorkletHandler = () => { + const scrollY = useSceneScrollContext(state => state.scrollY) + + // Create shared values for scroll position + const isFocused = useIsFocused() + + const localScrollY = useSharedValue(0) + + useAnimatedReaction( + () => { + return isFocused + }, + isFocusedResult => { + if (isFocusedResult && localScrollY.value !== scrollY.value) { + scrollY.value = localScrollY.value + } + } + ) + + // Define the handleScroll function as a worklet using useWorkletCallback + const handleScroll = useWorkletCallback((event: NativeScrollEvent) => { + 'worklet' + if (!isFocused) return + + const y = event.contentOffset.y + if (scrollY.value !== y) { + localScrollY.value = y + scrollY.value = y + } + }, []) + + return handleScroll +} diff --git a/yarn.lock b/yarn.lock index 8346fcc5d1d..6c0179b83c6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1100,7 +1100,7 @@ "@babel/types" "^7.4.4" esutils "^2.0.2" -"@babel/preset-typescript@^7.13.0", "@babel/preset-typescript@^7.16.7", "@babel/preset-typescript@^7.17.12", "@babel/preset-typescript@^7.18.6": +"@babel/preset-typescript@^7.13.0", "@babel/preset-typescript@^7.16.7", "@babel/preset-typescript@^7.18.6": version "7.24.1" resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.24.1.tgz#89bdf13a3149a17b3b2a2c9c62547f06db8845ec" integrity sha512-1DBaMmRDpuYQBPWD8Pf/WEwCrtgRHxsZnP4mIy9G/X+hFfbI47Q2G4t1Paakld84+qsk2fSsUPMKg71jkoOOaQ== @@ -16423,13 +16423,6 @@ react-native-device-info@^13.2.0: resolved "https://registry.yarnpkg.com/react-native-device-info/-/react-native-device-info-13.2.0.tgz#2b4026daf59dd08d82b6d65b63c18cb26f310ed2" integrity sha512-VpTxHZsEZ7kes2alaZkB31278KuSPXfTZ4TmCCN77+bYxNnaHUDiBiQ1TSoKAOp51b7gZ/7EvM4McfgHofcTBQ== -react-native-draggable-flatlist@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/react-native-draggable-flatlist/-/react-native-draggable-flatlist-4.0.1.tgz#2f027d387ba4b8f3eb0907340e32cb85e6460df2" - integrity sha512-ZO1QUTNx64KZfXGXeXcBfql67l38X7kBcJ3rxUVZzPHt5r035GnGzIC0F8rqSXp6zgnwgUYMfB6zQc5PKmPL9Q== - dependencies: - "@babel/preset-typescript" "^7.17.12" - react-native-email-link@^1.14.5: version "1.14.5" resolved "https://registry.yarnpkg.com/react-native-email-link/-/react-native-email-link-1.14.5.tgz#05e13002bd850ebaf98477b4cfbfdfa972a57608" @@ -16581,6 +16574,11 @@ react-native-reanimated@^3.14.0: convert-source-map "^2.0.0" invariant "^2.2.4" +react-native-reorderable-list@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/react-native-reorderable-list/-/react-native-reorderable-list-0.5.0.tgz#5f85360d68988fdd350cea720b0201f413329101" + integrity sha512-p830KvMJP8r2nOsdEyGlNW6F1/CA4bvCTCoFsZR+HJ3RFjujHIX3C8tf+Sl75wFGBUlPd5SSuJMZ0t85wUJYZA== + react-native-safari-view@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/react-native-safari-view/-/react-native-safari-view-2.1.0.tgz#1e0cd12c62bce79bc1759c7e281646b08b61c959"