diff --git a/src/components/AnimatedComponents/AnimatedFlatList.ts b/src/components/AnimatedComponents/AnimatedFlatList.ts deleted file mode 100644 index 6fe82c3740d..00000000000 --- a/src/components/AnimatedComponents/AnimatedFlatList.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { FlatList } from 'react-native-gesture-handler'; -import Animated from 'react-native-reanimated'; - -export const AnimatedFlatList = Animated.createAnimatedComponent(FlatList); diff --git a/src/components/DappBrowser/BrowserTab.tsx b/src/components/DappBrowser/BrowserTab.tsx index 893b0c7f86b..b6fee2bbc05 100644 --- a/src/components/DappBrowser/BrowserTab.tsx +++ b/src/components/DappBrowser/BrowserTab.tsx @@ -123,7 +123,7 @@ const HomepageOrWebView = ({ const isOnHomepage = useBrowserStore(state => !state.getTabData?.(tabId)?.url || state.getTabData?.(tabId)?.url === RAINBOW_HOME); return isOnHomepage ? ( - + ) : ( { +export const Homepage = () => { const { goToUrl } = useBrowserContext(); const { isDarkMode } = useColorMode(); - const backgroundStyle = isDarkMode ? styles.pageBackgroundDark : styles.pageBackgroundLight; - return ( - + - + - + - + ); @@ -122,90 +114,11 @@ const Trending = ({ goToUrl }: { goToUrl: (url: string) => void }) => { ); }; -const Favorites = ({ goToUrl, tabId }: { goToUrl: (url: string) => void; tabId: string }) => { - const { animatedTabUrls, activeTabInfo, currentlyOpenTabIds, tabViewProgress, tabViewVisible } = useBrowserContext(); - - const [localGridSort, setLocalGridSort] = useState(() => { - const orderedIds = useFavoriteDappsStore.getState().getOrderedIds(); - return orderedIds && orderedIds.length > 0 ? orderedIds : undefined; - }); - - const favoriteDapps = useFavoriteDappsStore(state => state.getFavorites(localGridSort)); - const gridKey = useMemo(() => localGridSort?.join('-'), [localGridSort]); - const isFirstRender = useRef(true); - - const reorderFavorites = useFavoriteDappsStore(state => state.reorderFavorites); - - const onGridOrderChange: DraggableGridProps['onOrderChange'] = useCallback( - (value: UniqueIdentifier[]) => { - reorderFavorites(value as string[]); - }, - [reorderFavorites] - ); - - const reinitializeGridSort = useCallback(() => { - setLocalGridSort(useFavoriteDappsStore.getState().getOrderedIds()); - }, []); - - const needsToSyncWorklet = useCallback( - ({ currentGridSort, isActiveTab }: { currentGridSort: string[] | undefined; isActiveTab: boolean }) => { - 'worklet'; - const homepageTabsCount = currentlyOpenTabIds.value.filter( - tabId => !animatedTabUrls.value[tabId] || animatedTabUrls.value[tabId] === DEFAULT_TAB_URL - ).length; - const inactiveAndMounted = !isActiveTab && currentGridSort !== undefined; - - if (homepageTabsCount === 1) { - if (inactiveAndMounted) return true; - return false; - } - - const activeAndUnmounted = isActiveTab && !currentGridSort; - - return activeAndUnmounted || inactiveAndMounted; - }, - [animatedTabUrls, currentlyOpenTabIds] - ); - - // Unmount drag and drop grid on inactive homepage tabs - useAnimatedReaction( - () => ({ - needsToSync: needsToSyncWorklet({ currentGridSort: localGridSort, isActiveTab: activeTabInfo.value.tabId === tabId }), - tabAnimationProgress: tabViewProgress.value, - }), - (current, previous) => { - if (!previous || (!current.needsToSync && current.tabAnimationProgress === previous.tabAnimationProgress) || !favoriteDapps.length) { - return; - } - - const enterTabViewAnimationIsComplete = - !tabViewVisible.value && previous.tabAnimationProgress < 0 && current.tabAnimationProgress >= 0; - - if (!enterTabViewAnimationIsComplete) return; - - if (activeTabInfo.value.tabId === tabId) { - runOnJS(reinitializeGridSort)(); - } else { - runOnJS(setLocalGridSort)(undefined); - } - }, - [] - ); - - // Reinitialize grid sort when favorites are added or removed - useLayoutEffect(() => { - if (isFirstRender.current) { - isFirstRender.current = false; - return; - } - if (!favoriteDapps.length || !useBrowserStore.getState().isTabActive(tabId)) { - return; - } - reinitializeGridSort(); - }, [favoriteDapps.length, reinitializeGridSort, tabId]); +const Favorites = ({ goToUrl }: { goToUrl: (url: string) => void }) => { + const favoriteDapps = useFavoriteDappsStore(state => state.favoriteDapps); return ( - + 􀋃 @@ -214,35 +127,14 @@ const Favorites = ({ goToUrl, tabId }: { goToUrl: (url: string) => void; tabId: {i18n.t(i18n.l.dapp_browser.homepage.favorites)} - {favoriteDapps.length > 0 && localGridSort ? ( - - - {favoriteDapps.map(dapp => - dapp ? ( - - - - ) : null - )} - - - ) : ( - - {favoriteDapps.length > 0 - ? favoriteDapps.map(dapp => ) - : Array(4) - .fill(null) - .map((_, index) => )} - - )} - + + {favoriteDapps.length > 0 + ? favoriteDapps.map(dapp => ) + : Array(4) + .fill(null) + .map((_, index) => )} + + ); }; @@ -272,7 +164,7 @@ const Recents = ({ goToUrl }: { goToUrl: (url: string) => void }) => { ); }; -const Card = memo(function Card({ +const Card = React.memo(function Card({ goToUrl, site, showMenuButton, @@ -391,7 +283,25 @@ const Card = memo(function Card({ width={{ custom: CARD_WIDTH }} > - + {(site.screenshot || dappIconUrl) && ( + + + + + + + )} - + {IS_IOS ? ( ) : ( @@ -451,19 +361,13 @@ const Card = memo(function Card({ - + 􀍠 @@ -475,47 +379,7 @@ const Card = memo(function Card({ ); }); -const CardBackground = memo(function CardBackgroundOverlay({ - imageUrl, - isDarkMode, -}: { - imageUrl: string | undefined; - isDarkMode: boolean; -}) { - return imageUrl ? ( - - - {IS_IOS ? ( - <> - - {!isDarkMode && ( - - )} - - ) : ( - - )} - - ) : null; -}); - -export const PlaceholderCard = memo(function PlaceholderCard() { +export const PlaceholderCard = React.memo(function PlaceholderCard() { const { isDarkMode } = useColorMode(); const fillTertiary = useBackgroundColor('fillTertiary'); @@ -553,69 +417,56 @@ export const PlaceholderCard = memo(function PlaceholderCard() { ); }); -export const Logo = memo(function Logo({ goToUrl, site }: { goToUrl: (url: string) => void; site: FavoritedSite }) { +export const Logo = React.memo(function Logo({ goToUrl, site }: { goToUrl: (url: string) => void; site: Omit }) { const { isDarkMode } = useColorMode(); - const imageOrFallback = useMemo(() => { - return ( - <> - {site.image && ( - - )} - - - - {!site.image && ( - - - 􀎭 - - - )} - - ); - }, [isDarkMode, site.image]); - return ( goToUrl(site.url)}> - {imageOrFallback} + + {IS_IOS && !site.image && ( + + + 􀎭 + + + )} + + {IS_IOS && ( + + )} + - + {site.name} @@ -626,13 +477,13 @@ export const Logo = memo(function Logo({ goToUrl, site }: { goToUrl: (url: strin ); }); -export const PlaceholderLogo = memo(function PlaceholderLogo() { +export const PlaceholderLogo = React.memo(function PlaceholderLogo() { const { isDarkMode } = useColorMode(); const borderRadius = IS_ANDROID ? LOGO_BORDER_RADIUS / 2 : LOGO_BORDER_RADIUS; return ( - + {IS_IOS && ( = { name: tabData?.title || '', url: tabData?.url || '', image: tabData?.logoUrl || '', diff --git a/src/components/DappBrowser/search-input/SearchInput.tsx b/src/components/DappBrowser/search-input/SearchInput.tsx index 68b1979fefc..7c880d75981 100644 --- a/src/components/DappBrowser/search-input/SearchInput.tsx +++ b/src/components/DappBrowser/search-input/SearchInput.tsx @@ -22,8 +22,9 @@ import { IS_IOS } from '@/env'; import * as i18n from '@/languages'; import { fontWithWidth } from '@/styles'; import font from '@/styles/fonts'; +import { Site } from '@/state/browserHistory'; import { useBrowserStore } from '@/state/browser/browserStore'; -import { FavoritedSite, useFavoriteDappsStore } from '@/state/browser/favoriteDappsStore'; +import { useFavoriteDappsStore } from '@/state/favoriteDapps'; import { GestureHandlerButton } from '@/__swaps__/screens/Swap/components/GestureHandlerButton'; import { THICK_BORDER_WIDTH } from '@/__swaps__/screens/Swap/constants'; import { FadeMask } from '@/__swaps__/screens/Swap/components/FadeMask'; @@ -63,7 +64,7 @@ const TheeDotMenu = function TheeDotMenu({ if (isFavorite) { removeFavorite(url); } else { - const site: FavoritedSite = { + const site: Omit = { name: getNameFromFormattedUrl(formattedUrlValue.value), url: url, image: useBrowserStore.getState().getActiveTabLogo() || `https://${formattedUrlValue.value}/apple-touch-icon.png`, diff --git a/src/components/drag-and-drop/DndContext.ts b/src/components/drag-and-drop/DndContext.ts deleted file mode 100644 index 6e58313f1c2..00000000000 --- a/src/components/drag-and-drop/DndContext.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { createContext, useContext, type RefObject } from 'react'; -import type { LayoutRectangle, View } from 'react-native'; -import type { GestureEventPayload } from 'react-native-gesture-handler'; -import type { SharedValue } from 'react-native-reanimated'; -import type { DraggableConstraints, SharedPoint } from './hooks'; -import type { SharedData, UniqueIdentifier } from './types'; - -export type ItemOptions = { id: UniqueIdentifier; data: SharedData; disabled: boolean }; -export type DraggableItemOptions = ItemOptions & DraggableConstraints; -export type DraggableOptions = Record; -export type DroppableOptions = Record; -export type Layouts = Record>; -export type Offsets = Record; -export type DraggableState = 'resting' | 'pending' | 'dragging' | 'dropping' | 'acting'; -export type DraggableStates = Record>; - -export type DndContextValue = { - containerRef: RefObject; - draggableLayouts: SharedValue; - droppableLayouts: SharedValue; - draggableOptions: SharedValue; - droppableOptions: SharedValue; - draggableOffsets: SharedValue; - draggableRestingOffsets: SharedValue; - draggableStates: SharedValue; - draggablePendingId: SharedValue; - draggableActiveId: SharedValue; - droppableActiveId: SharedValue; - draggableActiveLayout: SharedValue; - draggableInitialOffset: SharedPoint; - draggableContentOffset: SharedPoint; - panGestureState: SharedValue; -}; - -// @ts-expect-error ignore detached state -export const DndContext = createContext(null); - -export const useDndContext = () => { - return useContext(DndContext); -}; diff --git a/src/components/drag-and-drop/DndProvider.tsx b/src/components/drag-and-drop/DndProvider.tsx deleted file mode 100644 index 5f0ad9553e6..00000000000 --- a/src/components/drag-and-drop/DndProvider.tsx +++ /dev/null @@ -1,428 +0,0 @@ -import React, { - ComponentType, - forwardRef, - MutableRefObject, - PropsWithChildren, - RefObject, - useCallback, - useImperativeHandle, - useMemo, - useRef, -} from 'react'; -import { LayoutRectangle, StyleProp, View, ViewStyle } from 'react-native'; -import { - Gesture, - GestureDetector, - GestureEventPayload, - GestureStateChangeEvent, - GestureUpdateEvent, - PanGesture, - PanGestureHandlerEventPayload, - State, -} from 'react-native-gesture-handler'; -import ReactNativeHapticFeedback, { HapticFeedbackTypes } from 'react-native-haptic-feedback'; -import { cancelAnimation, runOnJS, useAnimatedReaction, useSharedValue, type WithSpringConfig } from 'react-native-reanimated'; -import { useAnimatedTimeout } from '@/hooks/reanimated/useAnimatedTimeout'; -import { - DndContext, - DraggableStates, - type DndContextValue, - type DraggableOptions, - type DroppableOptions, - type ItemOptions, - type Layouts, - type Offsets, -} from './DndContext'; -import { useSharedPoint } from './hooks'; -import type { UniqueIdentifier } from './types'; -import { animatePointWithSpring, applyOffset, getDistance, includesPoint, overlapsRectangle, Point, Rectangle } from './utils'; - -export type DndProviderProps = { - activationDelay?: number; - debug?: boolean; - disabled?: boolean; - gestureRef?: MutableRefObject; - hapticFeedback?: HapticFeedbackTypes; - minDistance?: number; - onBegin?: ( - event: GestureStateChangeEvent, - meta: { activeId: UniqueIdentifier; activeLayout: LayoutRectangle } - ) => void; - onDragEnd?: (event: { active: ItemOptions; over: ItemOptions | null }) => void; - onFinalize?: ( - event: GestureStateChangeEvent, - meta: { activeId: UniqueIdentifier; activeLayout: LayoutRectangle } - ) => void; - onUpdate?: ( - event: GestureUpdateEvent, - meta: { activeId: UniqueIdentifier; activeLayout: LayoutRectangle } - ) => void; - simultaneousHandlers?: RefObject>; - springConfig?: WithSpringConfig; - style?: StyleProp; - waitFor?: RefObject>; -}; - -export type DndProviderHandle = Pick< - DndContextValue, - 'draggableLayouts' | 'draggableOffsets' | 'draggableRestingOffsets' | 'draggableActiveId' ->; - -export const DndProvider = forwardRef>(function DndProvider( - { - activationDelay = 0, - children, - debug, - disabled, - gestureRef, - hapticFeedback, - minDistance = 0, - onBegin, - onDragEnd, - onFinalize, - onUpdate, - simultaneousHandlers, - springConfig = {}, - style, - waitFor, - }, - ref -) { - const containerRef = useRef(null); - const draggableLayouts = useSharedValue({}); - const droppableLayouts = useSharedValue({}); - const draggableOptions = useSharedValue({}); - const droppableOptions = useSharedValue({}); - const draggableOffsets = useSharedValue({}); - const draggableRestingOffsets = useSharedValue({}); - const draggableStates = useSharedValue({}); - const draggablePendingId = useSharedValue(null); - const draggableActiveId = useSharedValue(null); - const droppableActiveId = useSharedValue(null); - const draggableActiveLayout = useSharedValue(null); - const draggableInitialOffset = useSharedPoint(0, 0); - const draggableContentOffset = useSharedPoint(0, 0); - const panGestureState = useSharedValue(0); - - const runFeedback = () => { - if (hapticFeedback) { - ReactNativeHapticFeedback.trigger(hapticFeedback); - } - }; - - useAnimatedReaction( - () => (hapticFeedback ? draggableActiveId.value : null), - current => { - if (current !== null) { - runOnJS(runFeedback)(); - } - }, - [] - ); - - const contextValue = useRef({ - containerRef, - draggableLayouts, - droppableLayouts, - draggableOptions, - droppableOptions, - draggableOffsets, - draggableRestingOffsets, - draggableStates, - draggablePendingId, - draggableActiveId, - droppableActiveId, - panGestureState, - draggableInitialOffset, - draggableActiveLayout, - draggableContentOffset, - }); - - useImperativeHandle( - ref, - () => { - return { - draggableLayouts, - draggableOffsets, - draggableRestingOffsets, - draggableActiveId, - }; - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [] - ); - - const setActiveId = useCallback(() => { - 'worklet'; - const id = draggablePendingId.value; - - if (id !== null) { - debug && console.log(`draggableActiveId.value = ${id}`); - draggableActiveId.value = id; - - const { value: layouts } = draggableLayouts; - const { value: offsets } = draggableOffsets; - const { value: activeLayout } = layouts[id]; - const activeOffset = offsets[id]; - - draggableActiveLayout.value = applyOffset(activeLayout, { - x: activeOffset.x.value, - y: activeOffset.y.value, - }); - draggableStates.value[id].value = 'dragging'; - } - }, [debug, draggableActiveId, draggableActiveLayout, draggableLayouts, draggableOffsets, draggablePendingId, draggableStates]); - - const { clearTimeout: clearActiveIdTimeout, start: setActiveIdWithDelay } = useAnimatedTimeout({ - delayMs: activationDelay, - onTimeoutWorklet: setActiveId, - }); - - const panGesture = useMemo(() => { - const findActiveLayoutId = (point: Point): UniqueIdentifier | null => { - 'worklet'; - const { x, y } = point; - const { value: layouts } = draggableLayouts; - const { value: offsets } = draggableOffsets; - const { value: options } = draggableOptions; - for (const [id, layout] of Object.entries(layouts)) { - // console.log({ [id]: floorLayout(layout.value) }); - const offset = offsets[id]; - const isDisabled = options[id].disabled; - if ( - !isDisabled && - includesPoint(layout.value, { - x: x - offset.x.value + draggableContentOffset.x.value, - y: y - offset.y.value + draggableContentOffset.y.value, - }) - ) { - return id; - } - } - return null; - }; - - const findDroppableLayoutId = (activeLayout: LayoutRectangle): UniqueIdentifier | null => { - 'worklet'; - const { value: layouts } = droppableLayouts; - const { value: options } = droppableOptions; - for (const [id, layout] of Object.entries(layouts)) { - // console.log({ [id]: floorLayout(layout.value) }); - const isDisabled = options[id].disabled; - if (!isDisabled && overlapsRectangle(activeLayout, layout.value)) { - return id; - } - } - return null; - }; - - const panGesture = Gesture.Pan() - .maxPointers(1) - .onBegin(event => { - const { state, x, y } = event; - debug && console.log('begin', { state, x, y }); - // Gesture is globally disabled - if (disabled) { - return; - } - // console.log("begin", { state, x, y }); - // Track current state for cancellation purposes - panGestureState.value = state; - const { value: layouts } = draggableLayouts; - const { value: offsets } = draggableOffsets; - const { value: restingOffsets } = draggableRestingOffsets; - const { value: options } = draggableOptions; - const { value: states } = draggableStates; - // for (const [id, offset] of Object.entries(offsets)) { - // console.log({ [id]: [offset.x.value, offset.y.value] }); - // } - // Find the active layout key under {x, y} - const activeId = findActiveLayoutId({ x, y }); - // Check if an item was actually selected - if (activeId !== null) { - // Record any ongoing current offset as our initial offset for the gesture - const activeLayout = layouts[activeId].value; - const activeOffset = offsets[activeId]; - const restingOffset = restingOffsets[activeId]; - const { value: activeState } = states[activeId]; - draggableInitialOffset.x.value = activeOffset.x.value; - draggableInitialOffset.y.value = activeOffset.y.value; - // Cancel the ongoing animation if we just reactivated an acting/dragging item - if (['dragging', 'acting'].includes(activeState)) { - cancelAnimation(activeOffset.x); - cancelAnimation(activeOffset.y); - // If not we should reset the resting offset to the current offset value - // But only if the item is not currently still animating - } else { - // active or pending - // Record current offset as our natural resting offset for the gesture - restingOffset.x.value = activeOffset.x.value; - restingOffset.y.value = activeOffset.y.value; - } - // Update activeId directly or with an optional delay - const { activationDelay } = options[activeId]; - - if (activationDelay > 0) { - draggablePendingId.value = activeId; - draggableStates.value[activeId].value = 'pending'; - setActiveIdWithDelay(); - } else { - draggableActiveId.value = activeId; - draggableActiveLayout.value = applyOffset(activeLayout, { - x: activeOffset.x.value, - y: activeOffset.y.value, - }); - draggableStates.value[activeId].value = 'dragging'; - } - - if (onBegin) { - onBegin(event, { activeId, activeLayout }); - } - } - }) - .onUpdate(event => { - // console.log(draggableStates.value); - const { state, translationX, translationY } = event; - debug && console.log('update', { state, translationX, translationY }); - // Track current state for cancellation purposes - panGestureState.value = state; - const { value: activeId } = draggableActiveId; - const { value: pendingId } = draggablePendingId; - const { value: options } = draggableOptions; - const { value: layouts } = draggableLayouts; - const { value: offsets } = draggableOffsets; - if (activeId === null) { - // Check if we are currently waiting for activation delay - if (pendingId !== null) { - const { activationTolerance } = options[pendingId]; - // Check if we've moved beyond the activation tolerance - const distance = getDistance(translationX, translationY); - if (distance > activationTolerance) { - draggablePendingId.value = null; - clearActiveIdTimeout(); - } - } - // Ignore item-free interactions - return; - } - // Update our active offset to pan the active item - const activeOffset = offsets[activeId]; - activeOffset.x.value = draggableInitialOffset.x.value + translationX; - activeOffset.y.value = draggableInitialOffset.y.value + translationY; - // Check potential droppable candidates - const activeLayout = layouts[activeId].value; - draggableActiveLayout.value = applyOffset(activeLayout, { - x: activeOffset.x.value, - y: activeOffset.y.value, - }); - droppableActiveId.value = findDroppableLayoutId(draggableActiveLayout.value); - if (onUpdate) { - onUpdate(event, { activeId, activeLayout: draggableActiveLayout.value }); - } - }) - .onFinalize(event => { - const { state, velocityX, velocityY } = event; - debug && console.log('finalize', { state, velocityX, velocityY }); - // Track current state for cancellation purposes - panGestureState.value = state; // can be `FAILED` or `ENDED` - const { value: activeId } = draggableActiveId; - const { value: pendingId } = draggablePendingId; - const { value: layouts } = draggableLayouts; - const { value: offsets } = draggableOffsets; - const { value: restingOffsets } = draggableRestingOffsets; - const { value: states } = draggableStates; - // Ignore item-free interactions - if (activeId === null) { - // Check if we were currently waiting for activation delay - if (pendingId !== null) { - draggablePendingId.value = null; - clearActiveIdTimeout(); - } - return; - } - // Reset interaction-related shared state for styling purposes - draggableActiveId.value = null; - if (onFinalize) { - const activeLayout = layouts[activeId].value; - const activeOffset = offsets[activeId]; - const updatedLayout = applyOffset(activeLayout, { - x: activeOffset.x.value, - y: activeOffset.y.value, - }); - onFinalize(event, { activeId, activeLayout: updatedLayout }); - } - // Callback - if (state !== State.FAILED && onDragEnd) { - const { value: dropActiveId } = droppableActiveId; - onDragEnd({ - active: draggableOptions.value[activeId], - over: dropActiveId !== null ? droppableOptions.value[dropActiveId] : null, - }); - } - // Reset droppable - droppableActiveId.value = null; - // Move back to initial position - const activeOffset = offsets[activeId]; - const restingOffset = restingOffsets[activeId]; - states[activeId].value = 'acting'; - const [targetX, targetY] = [restingOffset.x.value, restingOffset.y.value]; - animatePointWithSpring( - activeOffset, - [targetX, targetY], - [ - { ...springConfig, velocity: velocityX / 4 }, - { ...springConfig, velocity: velocityY / 4 }, - ], - () => { - // Cancel if we are interacting again with this item - if (panGestureState.value !== State.END && panGestureState.value !== State.FAILED && states[activeId].value !== 'acting') { - return; - } - if (states[activeId]) { - states[activeId].value = 'resting'; - } - // for (const [id, offset] of Object.entries(offsets)) { - // console.log({ [id]: [offset.x.value.toFixed(2), offset.y.value.toFixed(2)] }); - // } - } - ); - }) - .withTestId('DndProvider.pan'); - - if (simultaneousHandlers) { - panGesture.simultaneousWithExternalGesture(simultaneousHandlers); - } - - if (waitFor) { - panGesture.requireExternalGestureToFail(waitFor); - } - - if (gestureRef) { - panGesture.withRef(gestureRef); - } - - // Duration in milliseconds of the LongPress gesture before Pan is allowed to activate. - // If the finger is moved during that period, the gesture will fail. - if (activationDelay > 0) { - panGesture.activateAfterLongPress(activationDelay); - } - - // Minimum distance the finger (or multiple fingers) need to travel before the gesture activates. Expressed in points. - if (minDistance > 0) { - panGesture.minDistance(minDistance); - } - - return panGesture; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [disabled]); - - return ( - - - - {children} - - - - ); -}); diff --git a/src/components/drag-and-drop/components/Draggable.tsx b/src/components/drag-and-drop/components/Draggable.tsx deleted file mode 100644 index 24e767646ad..00000000000 --- a/src/components/drag-and-drop/components/Draggable.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import React, { type FunctionComponent, type PropsWithChildren } from 'react'; -import { type ViewProps } from 'react-native'; -import Animated, { useAnimatedStyle, withTiming, type AnimatedProps } from 'react-native-reanimated'; -import { TIMING_CONFIGS } from '@/components/animations/animationConfigs'; -import { IS_IOS } from '@/env'; -import { useDraggable, type DraggableConstraints, type UseDroppableOptions } from '../hooks'; -import type { AnimatedStyleWorklet } from '../types'; - -export type DraggableProps = AnimatedProps & - UseDroppableOptions & - Partial & { - animatedStyleWorklet?: AnimatedStyleWorklet; - activeOpacity?: number; - activeScale?: number; - dragDirection?: 'x' | 'y'; - }; - -/** - * Draggable is a React functional component that can be used to create elements that can be dragged within a Drag and Drop context. - * - * @component - * @example - * - * Drag me! - * - * - * @param {object} props - The properties that define the Draggable component. - * @param {number} props.activationDelay - A number representing the duration, in milliseconds, that this draggable item needs to be held for before allowing a drag to start. - * @param {number} props.activationTolerance - A number representing the distance, in pixels, of motion that is tolerated before the drag operation is aborted. - * @param {number} props.activeOpacity - A number that defines the opacity of the Draggable component when it is active. - * @param {number} props.activeScale - A number that defines the scale of the Draggable component when it is active. - * @param {Function} props.animatedStyleWorklet - A worklet function that modifies the animated style of the Draggable component. - * @param {object} props.data - An object that contains data associated with the Draggable component. - * @param {boolean} props.disabled - A flag that indicates whether the Draggable component is disabled. - * @param {string} props.dragDirection - Locks the drag direction to the x or y axis. - * @param {string} props.id - A unique identifier for the Draggable component. - * @param {object} props.style - An object that defines the style of the Draggable component. - * @returns {React.Component} Returns a Draggable component that can be moved by the user within a Drag and Drop context. - */ -export const Draggable: FunctionComponent> = ({ - activationDelay, - activationTolerance, - activeOpacity = 1, - activeScale = 1.05, - animatedStyleWorklet, - children, - data, - disabled, - dragDirection, - id, - style, - ...otherProps -}) => { - const { setNodeRef, onLayout, onLayoutWorklet, offset, state } = useDraggable({ - id, - data, - disabled, - activationDelay, - activationTolerance, - }); - - const animatedStyle = useAnimatedStyle(() => { - const isActive = state.value === 'dragging'; - const isActing = state.value === 'acting'; - // eslint-disable-next-line no-nested-ternary - const zIndex = isActive ? 999 : isActing ? 998 : 1; - const style = { - opacity: withTiming(isActive ? activeOpacity : 1, TIMING_CONFIGS.tabPressConfig), - zIndex, - transform: [ - { - translateX: - // eslint-disable-next-line no-nested-ternary - dragDirection !== 'y' ? (isActive ? offset.x.value : withTiming(offset.x.value, TIMING_CONFIGS.slowestFadeConfig)) : 0, - }, - { - translateY: - // eslint-disable-next-line no-nested-ternary - dragDirection !== 'x' ? (isActive ? offset.y.value : withTiming(offset.y.value, TIMING_CONFIGS.slowestFadeConfig)) : 0, - }, - { scale: activeScale === undefined ? 1 : withTiming(isActive ? activeScale : 1, TIMING_CONFIGS.tabPressConfig) }, - ], - }; - if (animatedStyleWorklet) { - Object.assign(style, animatedStyleWorklet(style, { isActive, isActing, isDisabled: !!disabled })); - } - return style; - }, [id, state, activeOpacity, activeScale]); - - return ( - - {children} - - ); -}; diff --git a/src/components/drag-and-drop/components/DraggableFlatList.tsx b/src/components/drag-and-drop/components/DraggableFlatList.tsx deleted file mode 100644 index 597d07b8df0..00000000000 --- a/src/components/drag-and-drop/components/DraggableFlatList.tsx +++ /dev/null @@ -1,236 +0,0 @@ -import React, { - ComponentProps, - ReactElement, - // useCallback, -} from 'react'; -import { - CellRendererProps, - // FlatListProps, -} from 'react-native'; -import { FlatList } from 'react-native-gesture-handler'; -import Animated, { - AnimatedProps, - // runOnJS, - useAnimatedReaction, - useAnimatedRef, - useAnimatedScrollHandler, - // useSharedValue, -} from 'react-native-reanimated'; -import { AnimatedFlatList } from '@/components/AnimatedComponents/AnimatedFlatList'; -import { useDndContext } from '../DndContext'; -import { useDraggableSort, UseDraggableStackOptions } from '../features'; -import type { UniqueIdentifier } from '../types'; -import { swapByItemCenterPoint } from '../utils'; -import { Draggable } from './Draggable'; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type AnimatedFlatListProps = AnimatedProps>>; - -export type ViewableRange = { - first: number | null; - last: number | null; -}; - -export type DraggableFlatListProps = AnimatedFlatListProps & - Pick & { - debug?: boolean; - gap?: number; - horizontal?: boolean; - initialOrder?: UniqueIdentifier[]; - }; - -export const DraggableFlatList = ({ - data, - // debug, - gap = 0, - horizontal = false, - initialOrder, - onOrderChange, - onOrderUpdate, - renderItem, - shouldSwapWorklet = swapByItemCenterPoint, - ...otherProps -}: DraggableFlatListProps): ReactElement => { - const { draggableActiveId, draggableContentOffset, draggableLayouts, draggableOffsets, draggableRestingOffsets } = useDndContext(); - const animatedFlatListRef = useAnimatedRef>(); - - const { - // draggablePlaceholderIndex, - draggableSortOrder, - } = useDraggableSort({ - horizontal, - initialOrder, - onOrderChange, - onOrderUpdate, - shouldSwapWorklet, - }); - - const direction = horizontal ? 'column' : 'row'; - const size = 1; - - // Track sort order changes and update the offsets - useAnimatedReaction( - () => draggableSortOrder.value, - (nextOrder, prevOrder) => { - // Ignore initial reaction - if (prevOrder === null) { - return; - } - const { value: activeId } = draggableActiveId; - const { value: layouts } = draggableLayouts; - const { value: offsets } = draggableOffsets; - const { value: restingOffsets } = draggableRestingOffsets; - if (!activeId) { - return; - } - - const activeLayout = layouts[activeId].value; - const { width, height } = activeLayout; - const restingOffset = restingOffsets[activeId]; - - for (let nextIndex = 0; nextIndex < nextOrder.length; nextIndex++) { - const itemId = nextOrder[nextIndex]; - const prevIndex = prevOrder.findIndex(id => id === itemId); - // Skip items that haven't changed position - if (nextIndex === prevIndex) { - continue; - } - - const prevRow = Math.floor(prevIndex / size); - const prevCol = prevIndex % size; - const nextRow = Math.floor(nextIndex / size); - const nextCol = nextIndex % size; - const moveCol = nextCol - prevCol; - const moveRow = nextRow - prevRow; - - const offset = itemId === activeId ? restingOffset : offsets[itemId]; - - if (!restingOffset || !offsets[itemId]) { - continue; - } - - switch (direction) { - case 'row': - offset.y.value += moveRow * (height + gap); - break; - case 'column': - offset.x.value += moveCol * (width + gap); - break; - default: - break; - } - } - }, - [] - ); - - const scrollHandler = useAnimatedScrollHandler(event => { - draggableContentOffset.y.value = event.contentOffset.y; - }); - - /** ⚠️ TODO: Implement auto scrolling when dragging above or below the visible range */ - // const scrollToIndex = useCallback( - // (index: number) => { - // animatedFlatListRef.current?.scrollToIndex({ - // index, - // viewPosition: 0, - // animated: true, - // }); - // }, - // [animatedFlatListRef] - // ); - - // const viewableRange = useSharedValue({ - // first: null, - // last: null, - // }); - - // const onViewableItemsChanged = useCallback['onViewableItemsChanged']>>( - // ({ viewableItems }) => { - // viewableRange.value = { - // first: viewableItems[0].index, - // last: viewableItems[viewableItems.length - 1].index, - // }; - // if (debug) - // console.log(`First viewable item index: ${viewableItems[0].index}, last: ${viewableItems[viewableItems.length - 1].index}`); - // }, - // [debug, viewableRange] - // ); - - // useAnimatedReaction( - // () => draggablePlaceholderIndex.value, - // (next, prev) => { - // if (!Array.isArray(data)) { - // return; - // } - // if (debug) console.log(`placeholderIndex: ${prev} -> ${next}}, last visible= ${viewableRange.value.last}`); - // const { - // value: { first, last }, - // } = viewableRange; - // if (last !== null && next >= last && last < data.length - 1) { - // if (next < data.length) { - // runOnJS(scrollToIndex)(next + 1); - // } - // } else if (first !== null && first > 0 && next <= first) { - // if (next > 0) { - // runOnJS(scrollToIndex)(next - 1); - // } - // } - // } - // ); - /** END */ - - /** 🛠️ DEBUGGING */ - // useAnimatedReaction( - // () => { - // const activeId = draggableActiveId.value; - // return activeId ? draggableStates.value[activeId].value : 'resting'; - // }, - // (next, prev) => { - // if (debug) console.log(`Active item state: ${prev} -> ${next}`); - // if (debug) console.log(`translationY.value=${draggableContentOffset.y.value}`); - // } - // ); - - // useAnimatedReaction( - // () => draggableActiveId.value, - // (next, prev) => { - // if (debug) console.log(`activeId: ${prev} -> ${next}`); - // } - // ); - /** END */ - - return ( - { - return ( - - ); - }} - viewabilityConfig={{ itemVisiblePercentThreshold: 50 }} - // eslint-disable-next-line react/jsx-props-no-spreading, @typescript-eslint/no-explicit-any - {...(otherProps as any)} - /> - ); -}; - -export const DraggableFlatListCellRenderer = function DraggableFlatListCellRenderer( - props: CellRendererProps -) { - const { item, children } = props; - return ( - - {children} - - ); -}; diff --git a/src/components/drag-and-drop/components/Droppable.tsx b/src/components/drag-and-drop/components/Droppable.tsx deleted file mode 100644 index 154a2e01dbc..00000000000 --- a/src/components/drag-and-drop/components/Droppable.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import React, { type FunctionComponent, type PropsWithChildren } from 'react'; -import { type ViewProps } from 'react-native'; -import Animated, { useAnimatedStyle, withTiming, type AnimatedProps } from 'react-native-reanimated'; -import { TIMING_CONFIGS } from '@/components/animations/animationConfigs'; -import { IS_IOS } from '@/env'; -import { useDroppable, type UseDraggableOptions } from '../hooks'; -import type { AnimatedStyleWorklet } from '../types'; - -export type DroppableProps = AnimatedProps & - UseDraggableOptions & { - animatedStyleWorklet?: AnimatedStyleWorklet; - activeOpacity?: number; - activeScale?: number; - }; - -/** - * Droppable is a React functional component that can be used to create a drop target in a Drag and Drop context. - * - * @component - * @example - * - * Drop here! - * - * - * @param {object} props - The properties that define the Droppable component. - * @param {string} props.id - A unique identifier for the Droppable component. - * @param {boolean} props.disabled - A flag that indicates whether the Droppable component is disabled. - * @param {object} props.data - An object that contains data associated with the Droppable component. - * @param {object} props.style - An object that defines the style of the Droppable component. - * @param {number} props.activeOpacity - A number that defines the opacity of the Droppable component when it is active. - * @param {number} props.activeScale - A number that defines the scale of the Droppable component when it is active. - * @param {Function} props.animatedStyleWorklet - A worklet function that modifies the animated style of the Droppable component. - * @returns {React.Component} Returns a Droppable component that can serve as a drop target within a Drag and Drop context. - */ -export const Droppable: FunctionComponent> = ({ - children, - id, - disabled, - data, - style, - activeOpacity = 0.9, - activeScale, - animatedStyleWorklet, - ...otherProps -}) => { - const { setNodeRef, onLayout, onLayoutWorklet, activeId } = useDroppable({ - id, - disabled, - data, - }); - - const animatedStyle = useAnimatedStyle(() => { - const isActive = activeId.value === id; - const style = { - opacity: withTiming(isActive ? activeOpacity : 1, TIMING_CONFIGS.tabPressConfig), - transform: [{ scale: activeScale === undefined ? 1 : withTiming(isActive ? activeScale : 1, TIMING_CONFIGS.tabPressConfig) }], - }; - if (animatedStyleWorklet) { - Object.assign(style, animatedStyleWorklet(style, { isActive, isDisabled: !!disabled })); - } - return style; - }, [id, activeOpacity, activeScale]); - - return ( - - {children} - - ); -}; diff --git a/src/components/drag-and-drop/components/index.ts b/src/components/drag-and-drop/components/index.ts deleted file mode 100644 index 88f40fe1543..00000000000 --- a/src/components/drag-and-drop/components/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './Draggable'; -export * from './DraggableFlatList'; -export * from './Droppable'; diff --git a/src/components/drag-and-drop/features/index.ts b/src/components/drag-and-drop/features/index.ts deleted file mode 100644 index b79f1a3e386..00000000000 --- a/src/components/drag-and-drop/features/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './sort'; diff --git a/src/components/drag-and-drop/features/sort/components/DraggableGrid.tsx b/src/components/drag-and-drop/features/sort/components/DraggableGrid.tsx deleted file mode 100644 index c6f9282c805..00000000000 --- a/src/components/drag-and-drop/features/sort/components/DraggableGrid.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import React, { Children, useMemo, type FunctionComponent, type PropsWithChildren } from 'react'; -import { StyleProp, View, ViewStyle, type FlexStyle, type ViewProps } from 'react-native'; -import type { UniqueIdentifier } from '../../../types'; -import { useDraggableGrid, type UseDraggableGridOptions } from '../hooks/useDraggableGrid'; - -export type DraggableGridProps = Pick & - Pick & { - direction?: FlexStyle['flexDirection']; - size: number; - gap?: number; - }; - -export const DraggableGrid: FunctionComponent> = ({ - children, - direction = 'row', - gap = 0, - onOrderChange, - onOrderUpdate, - shouldSwapWorklet, - size, - style: styleProp, -}) => { - const initialOrder = useMemo( - () => - Children.map(children, child => { - if (React.isValidElement(child)) { - return child.props.id; - } - return null; - })?.filter(Boolean) as UniqueIdentifier[], - [children] - ); - - const style: StyleProp = useMemo( - () => ({ - flexDirection: direction, - gap, - flexWrap: 'wrap', - ...(styleProp as object), - }), - [gap, direction, styleProp] - ); - - useDraggableGrid({ - direction: style.flexDirection, - gap: style.gap, - initialOrder, - onOrderChange, - onOrderUpdate, - shouldSwapWorklet, - size, - }); - - return {children}; -}; diff --git a/src/components/drag-and-drop/features/sort/components/DraggableStack.tsx b/src/components/drag-and-drop/features/sort/components/DraggableStack.tsx deleted file mode 100644 index 34dddffd0d2..00000000000 --- a/src/components/drag-and-drop/features/sort/components/DraggableStack.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import React, { Children, useMemo, type FunctionComponent, type PropsWithChildren } from 'react'; -import { View, type FlexStyle, type ViewProps } from 'react-native'; -import type { UniqueIdentifier } from '../../../types'; -import { useDraggableStack, type UseDraggableStackOptions } from '../hooks/useDraggableStack'; - -export type DraggableStackProps = Pick & - Pick & { - direction?: FlexStyle['flexDirection']; - gap?: number; - }; - -export const DraggableStack: FunctionComponent> = ({ - children, - direction = 'row', - gap = 0, - onOrderChange, - onOrderUpdate, - shouldSwapWorklet, - style: styleProp, -}) => { - const initialOrder = useMemo( - () => - Children.map(children, child => { - // console.log("in"); - if (React.isValidElement(child)) { - return child.props.id; - } - return null; - })?.filter(Boolean) as UniqueIdentifier[], - [children] - ); - - const style = useMemo( - () => ({ - flexDirection: direction, - gap, - ...(styleProp as object), - }), - [gap, direction, styleProp] - ); - - const horizontal = ['row', 'row-reverse'].includes(style.flexDirection); - - useDraggableStack({ - gap: style.gap, - horizontal, - initialOrder, - onOrderChange, - onOrderUpdate, - shouldSwapWorklet, - }); - - return {children}; -}; diff --git a/src/components/drag-and-drop/features/sort/components/index.ts b/src/components/drag-and-drop/features/sort/components/index.ts deleted file mode 100644 index ec52d14a680..00000000000 --- a/src/components/drag-and-drop/features/sort/components/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './DraggableGrid'; -export * from './DraggableStack'; diff --git a/src/components/drag-and-drop/features/sort/hooks/index.ts b/src/components/drag-and-drop/features/sort/hooks/index.ts deleted file mode 100644 index 0ce999dc5ab..00000000000 --- a/src/components/drag-and-drop/features/sort/hooks/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './useDraggableGrid'; -export * from './useDraggableSort'; -export * from './useDraggableStack'; diff --git a/src/components/drag-and-drop/features/sort/hooks/useDraggableGrid.ts b/src/components/drag-and-drop/features/sort/hooks/useDraggableGrid.ts deleted file mode 100644 index af298b207c4..00000000000 --- a/src/components/drag-and-drop/features/sort/hooks/useDraggableGrid.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { type FlexStyle } from 'react-native'; -import { useAnimatedReaction } from 'react-native-reanimated'; -import { swapByItemCenterPoint } from '../../../utils'; -import { useDndContext } from './../../../DndContext'; -import { useDraggableSort, type UseDraggableSortOptions } from './useDraggableSort'; - -export type UseDraggableGridOptions = Pick< - UseDraggableSortOptions, - 'initialOrder' | 'onOrderChange' | 'onOrderUpdate' | 'shouldSwapWorklet' -> & { - gap?: number; - size: number; - direction: FlexStyle['flexDirection']; -}; - -export const useDraggableGrid = ({ - initialOrder, - onOrderChange, - onOrderUpdate, - gap = 0, - size, - direction = 'row', - shouldSwapWorklet = swapByItemCenterPoint, -}: UseDraggableGridOptions) => { - const { draggableActiveId, draggableOffsets, draggableRestingOffsets, draggableLayouts } = useDndContext(); - const horizontal = ['row', 'row-reverse'].includes(direction); - - const { draggablePlaceholderIndex, draggableSortOrder } = useDraggableSort({ - horizontal, - initialOrder, - onOrderChange, - onOrderUpdate, - shouldSwapWorklet, - }); - - // Track sort order changes and update the offsets - useAnimatedReaction( - () => draggableSortOrder.value, - (nextOrder, prevOrder) => { - // Ignore initial reaction - if (prevOrder === null) { - return; - } - const { value: activeId } = draggableActiveId; - const { value: layouts } = draggableLayouts; - const { value: offsets } = draggableOffsets; - const { value: restingOffsets } = draggableRestingOffsets; - if (!activeId) { - return; - } - - const activeLayout = layouts[activeId].value; - const { width, height } = activeLayout; - const restingOffset = restingOffsets[activeId]; - - for (let nextIndex = 0; nextIndex < nextOrder.length; nextIndex++) { - const itemId = nextOrder[nextIndex]; - const prevIndex = prevOrder.findIndex(id => id === itemId); - // Skip items that haven't changed position - if (nextIndex === prevIndex) { - continue; - } - - const prevRow = Math.floor(prevIndex / size); - const prevCol = prevIndex % size; - const nextRow = Math.floor(nextIndex / size); - const nextCol = nextIndex % size; - const moveCol = nextCol - prevCol; - const moveRow = nextRow - prevRow; - - const offset = itemId === activeId ? restingOffset : offsets[itemId]; - - if (!restingOffset || !offsets[itemId]) { - continue; - } - - switch (direction) { - case 'row': - offset.x.value += moveCol * (width + gap); - offset.y.value += moveRow * (height + gap); - break; - case 'row-reverse': - offset.x.value += -1 * moveCol * (width + gap); - offset.y.value += moveRow * (height + gap); - break; - case 'column': - offset.y.value += moveCol * (width + gap); - offset.x.value += moveRow * (height + gap); - break; - case 'column-reverse': - offset.y.value += -1 * moveCol * (width + gap); - offset.x.value += moveRow * (height + gap); - break; - default: - break; - } - } - }, - [direction, gap, size] - ); - - return { draggablePlaceholderIndex, draggableSortOrder }; -}; diff --git a/src/components/drag-and-drop/features/sort/hooks/useDraggableSort.ts b/src/components/drag-and-drop/features/sort/hooks/useDraggableSort.ts deleted file mode 100644 index 7bdf88469f8..00000000000 --- a/src/components/drag-and-drop/features/sort/hooks/useDraggableSort.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { LayoutRectangle } from 'react-native'; -import { runOnJS, useAnimatedReaction, useSharedValue } from 'react-native-reanimated'; -import { useDndContext } from '../../../DndContext'; -import type { UniqueIdentifier } from '../../../types'; -import { applyOffset, arraysEqual, centerAxis, moveArrayIndex, overlapsAxis, type Rectangle } from '../../../utils'; - -export type ShouldSwapWorklet = (activeLayout: Rectangle, itemLayout: Rectangle) => boolean; - -export type UseDraggableSortOptions = { - initialOrder?: UniqueIdentifier[]; - horizontal?: boolean; - onOrderChange?: (order: UniqueIdentifier[]) => void; - onOrderUpdate?: (nextOrder: UniqueIdentifier[], prevOrder: UniqueIdentifier[]) => void; - shouldSwapWorklet?: ShouldSwapWorklet; -}; - -export const useDraggableSort = ({ - horizontal = false, - initialOrder = [], - onOrderChange, - onOrderUpdate, - shouldSwapWorklet, -}: UseDraggableSortOptions) => { - const { draggableActiveId, draggableActiveLayout, draggableOffsets, draggableLayouts } = useDndContext(); - - const draggablePlaceholderIndex = useSharedValue(-1); - const draggableLastOrder = useSharedValue(initialOrder); - const draggableSortOrder = useSharedValue(initialOrder); - - // Core placeholder index logic - const findPlaceholderIndex = (activeLayout: LayoutRectangle): number => { - 'worklet'; - const { value: activeId } = draggableActiveId; - const { value: layouts } = draggableLayouts; - const { value: offsets } = draggableOffsets; - const { value: sortOrder } = draggableSortOrder; - const activeIndex = sortOrder.findIndex(id => id === activeId); - // const activeCenterPoint = centerPoint(activeLayout); - // console.log(`activeLayout: ${JSON.stringify(activeLayout)}`); - for (let itemIndex = 0; itemIndex < sortOrder.length; itemIndex++) { - const itemId = sortOrder[itemIndex]; - if (itemId === activeId) { - continue; - } - if (!layouts[itemId]) { - console.warn(`Unexpected missing layout ${itemId} in layouts!`); - continue; - } - const itemLayout = applyOffset(layouts[itemId].value, { - x: offsets[itemId].x.value, - y: offsets[itemId].y.value, - }); - - if (shouldSwapWorklet) { - if (shouldSwapWorklet(activeLayout, itemLayout)) { - // console.log(`Found placeholder index ${itemIndex} using custom shouldSwapWorklet!`); - return itemIndex; - } - continue; - } - - // Default to center axis - const itemCenterAxis = centerAxis(itemLayout, horizontal); - if (overlapsAxis(activeLayout, itemCenterAxis, horizontal)) { - return itemIndex; - } - } - // Fallback to current index - // console.log(`Fallback to current index ${activeIndex}`); - return activeIndex; - }; - - // Track active layout changes and update the placeholder index - useAnimatedReaction( - () => [draggableActiveId.value, draggableActiveLayout.value] as const, - ([nextActiveId, nextActiveLayout], prev) => { - // Ignore initial reaction - if (prev === null) { - return; - } - // const [_prevActiveId, _prevActiveLayout] = prev; - // No active layout - if (nextActiveLayout === null) { - return; - } - // Reset the placeholder index when the active id changes - if (nextActiveId === null) { - draggablePlaceholderIndex.value = -1; - return; - } - // const axis = direction === "row" ? "x" : "y"; - // const delta = prevActiveLayout !== null ? nextActiveLayout[axis] - prevActiveLayout[axis] : 0; - draggablePlaceholderIndex.value = findPlaceholderIndex(nextActiveLayout); - }, - [] - ); - - // Track placeholder index changes and update the sort order - useAnimatedReaction( - () => [draggableActiveId.value, draggablePlaceholderIndex.value] as const, - (next, prev) => { - // Ignore initial reaction - if (prev === null) { - return; - } - const [, prevPlaceholderIndex] = prev; - const [nextActiveId, nextPlaceholderIndex] = next; - const { value: prevOrder } = draggableSortOrder; - // if (nextPlaceholderIndex !== prevPlaceholderIndex) { - // console.log(`Placeholder index changed from ${prevPlaceholderIndex} to ${nextPlaceholderIndex}`); - // } - if (prevPlaceholderIndex !== -1 && nextPlaceholderIndex === -1) { - // Notify the parent component of the order change - if (nextActiveId === null && onOrderChange) { - if (!arraysEqual(prevOrder, draggableLastOrder.value)) { - runOnJS(onOrderChange)(prevOrder); - } - draggableLastOrder.value = prevOrder; - } - } - // Only update the sort order when the placeholder index changes between two valid values - if (prevPlaceholderIndex === -1 || nextPlaceholderIndex === -1) { - return; - } - // Finally update the sort order - const nextOrder = moveArrayIndex(prevOrder, prevPlaceholderIndex, nextPlaceholderIndex); - // Notify the parent component of the order update - if (onOrderUpdate) { - runOnJS(onOrderUpdate)(nextOrder, prevOrder); - } - - draggableSortOrder.value = nextOrder; - }, - [onOrderChange] - ); - - return { draggablePlaceholderIndex, draggableSortOrder }; -}; diff --git a/src/components/drag-and-drop/features/sort/hooks/useDraggableStack.ts b/src/components/drag-and-drop/features/sort/hooks/useDraggableStack.ts deleted file mode 100644 index cb6626aab85..00000000000 --- a/src/components/drag-and-drop/features/sort/hooks/useDraggableStack.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { useMemo } from 'react'; -import { useAnimatedReaction } from 'react-native-reanimated'; -import { useDndContext } from '../../../DndContext'; -import { swapByItemHorizontalAxis, swapByItemVerticalAxis } from '../../../utils'; -import { useDraggableSort, type UseDraggableSortOptions } from './useDraggableSort'; - -export type UseDraggableStackOptions = Pick< - UseDraggableSortOptions, - 'initialOrder' | 'onOrderChange' | 'onOrderUpdate' | 'shouldSwapWorklet' -> & { - gap?: number; - horizontal?: boolean; -}; -export const useDraggableStack = ({ - initialOrder, - onOrderChange, - onOrderUpdate, - gap = 0, - horizontal = false, - shouldSwapWorklet, -}: UseDraggableStackOptions) => { - const { draggableActiveId, draggableOffsets, draggableRestingOffsets, draggableLayouts } = useDndContext(); - const axis = horizontal ? 'x' : 'y'; - const size = horizontal ? 'width' : 'height'; - const worklet = useMemo( - () => - // eslint-disable-next-line no-nested-ternary - shouldSwapWorklet ? shouldSwapWorklet : horizontal ? swapByItemHorizontalAxis : swapByItemVerticalAxis, - [horizontal, shouldSwapWorklet] - ); - - const { draggablePlaceholderIndex, draggableSortOrder } = useDraggableSort({ - horizontal, - initialOrder, - onOrderChange, - onOrderUpdate, - shouldSwapWorklet: worklet, - }); - - // Track sort order changes and update the offsets - useAnimatedReaction( - () => draggableSortOrder.value, - (nextOrder, prevOrder) => { - // Ignore initial reaction - if (prevOrder === null) { - return; - } - const { value: activeId } = draggableActiveId; - const { value: layouts } = draggableLayouts; - const { value: offsets } = draggableOffsets; - const { value: restingOffsets } = draggableRestingOffsets; - if (!activeId) { - return; - } - - const activeLayout = layouts[activeId].value; - const prevActiveIndex = prevOrder.findIndex(id => id === activeId); - const nextActiveIndex = nextOrder.findIndex(id => id === activeId); - const nextActiveOffset = { x: 0, y: 0 }; - const restingOffset = restingOffsets[activeId]; - - for (let nextIndex = 0; nextIndex < nextOrder.length; nextIndex++) { - const itemId = nextOrder[nextIndex]; - // Skip the active item - if (itemId === activeId) { - continue; - } - // @TODO grid x,y - - // Skip items that haven't changed position - const prevIndex = prevOrder.findIndex(id => id === itemId); - if (nextIndex === prevIndex) { - continue; - } - // Calculate the directional offset - const moveCol = nextIndex - prevIndex; - // Apply the offset to the item from its resting position - offsets[itemId][axis].value = restingOffsets[itemId][axis].value + moveCol * (activeLayout[size] + gap); - // Reset resting offsets to new updated position - restingOffsets[itemId][axis].value = offsets[itemId][axis].value; - - // Accummulate the directional offset for the active item - if (nextIndex < nextActiveIndex && prevIndex > prevActiveIndex) { - nextActiveOffset[axis] += layouts[itemId].value[size] + gap; - } else if (nextIndex > nextActiveIndex && prevIndex < prevActiveIndex) { - nextActiveOffset[axis] -= layouts[itemId].value[size] + gap; - } - } - // Update the active item offset - restingOffset[axis].value += nextActiveOffset[axis]; - }, - [horizontal] - ); - - return { draggablePlaceholderIndex, draggableSortOrder }; -}; diff --git a/src/components/drag-and-drop/features/sort/index.ts b/src/components/drag-and-drop/features/sort/index.ts deleted file mode 100644 index f76fd6f166e..00000000000 --- a/src/components/drag-and-drop/features/sort/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './components'; -export * from './hooks'; diff --git a/src/components/drag-and-drop/hooks/index.ts b/src/components/drag-and-drop/hooks/index.ts deleted file mode 100644 index 26087b68cc4..00000000000 --- a/src/components/drag-and-drop/hooks/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -export * from './useActiveDragReaction'; -export * from './useActiveDropReaction'; -export * from './useDraggable'; -export * from './useDraggableActiveId'; -export * from './useDraggableStyle'; -export * from './useDroppable'; -export * from './useDroppableStyle'; -export * from './useEvent'; -export * from './useLatestSharedValue'; -export * from './useLatestValue'; -export * from './useNodeRef'; -export * from './useSharedPoint'; -export * from './useSharedValuePair'; diff --git a/src/components/drag-and-drop/hooks/useActiveDragReaction.ts b/src/components/drag-and-drop/hooks/useActiveDragReaction.ts deleted file mode 100644 index cd55cd87c8a..00000000000 --- a/src/components/drag-and-drop/hooks/useActiveDragReaction.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { State } from 'react-native-gesture-handler'; -import { useAnimatedReaction } from 'react-native-reanimated'; -import { useDndContext } from '../DndContext'; -import type { UniqueIdentifier } from '../types'; - -export const useActiveDragReaction = (id: UniqueIdentifier, callback: (isActive: boolean) => void) => { - const { draggableActiveId: activeId, panGestureState } = useDndContext(); - useAnimatedReaction( - () => activeId.value === id && ([State.BEGAN, State.ACTIVE] as State[]).includes(panGestureState.value), - (next, prev) => { - if (next !== prev) { - callback(next); - } - }, - [] - ); -}; diff --git a/src/components/drag-and-drop/hooks/useActiveDropReaction.ts b/src/components/drag-and-drop/hooks/useActiveDropReaction.ts deleted file mode 100644 index c7be27157b9..00000000000 --- a/src/components/drag-and-drop/hooks/useActiveDropReaction.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { useAnimatedReaction } from 'react-native-reanimated'; -import { useDndContext } from '../DndContext'; -import type { UniqueIdentifier } from '../types'; - -export const useActiveDropReaction = (id: UniqueIdentifier, callback: (isActive: boolean) => void) => { - const { droppableActiveId: activeId } = useDndContext(); - useAnimatedReaction( - () => activeId.value === id, - (next, prev) => { - if (next !== prev) { - callback(next); - } - }, - [] - ); -}; diff --git a/src/components/drag-and-drop/hooks/useDraggable.ts b/src/components/drag-and-drop/hooks/useDraggable.ts deleted file mode 100644 index d27ee31da55..00000000000 --- a/src/components/drag-and-drop/hooks/useDraggable.ts +++ /dev/null @@ -1,177 +0,0 @@ -import { useCallback, useLayoutEffect } from 'react'; -import { LayoutRectangle, ViewProps } from 'react-native'; -import { runOnUI, useSharedValue } from 'react-native-reanimated'; -import { IS_IOS } from '@/env'; -import { useLayoutWorklet } from '@/hooks/reanimated/useLayoutWorklet'; -import { DraggableState, useDndContext } from '../DndContext'; -import { useLatestSharedValue, useNodeRef } from '../hooks'; -import { Data, NativeElement, UniqueIdentifier } from '../types'; -import { assert, isReanimatedSharedValue } from '../utils'; -import { useSharedPoint } from './useSharedPoint'; - -export type DraggableConstraints = { - activationDelay: number; - activationTolerance: number; -}; - -export type UseDraggableOptions = Partial & { - id: UniqueIdentifier; - data?: Data; - disabled?: boolean; -}; - -/** - * useDraggable is a custom hook that provides functionality for making a component draggable within a drag and drop context. - * - * @function - * @example - * const { offset, setNodeRef, activeId, setNodeLayout, draggableState } = useDraggable({ id: 'draggable-1' }); - * - * @param {object} options - The options that define the behavior of the draggable component. - * @param {string} options.id - A unique identifier for the draggable component. - * @param {object} [options.data={}] - Optional data associated with the draggable component. - * @param {boolean} [options.disabled=false] - A flag that indicates whether the draggable component is disabled. - * @param {number} [options.activationDelay=0] - A number representing the duration, in milliseconds, that this draggable item needs to be held for before allowing a drag to start. - * @param {number} [options.activationTolerance=Infinity] - A number representing the distance, in points, of motion that is tolerated before the drag operation is aborted. - * - * @returns {object} Returns an object with properties and methods related to the draggable component. - * @property {object} offset - An object representing the current offset of the draggable component. - * @property {Function} setNodeRef - A function that can be used to set the ref of the draggable component. - * @property {string} activeId - The unique identifier of the currently active draggable component. - * @property {string} actingId - The unique identifier of the currently interacti draggable component. - * @property {Function} setNodeLayout - A function that handles the layout event of the draggable component. - * @property {object} draggableState - An object representing the current state of the draggable component. - */ -export const useDraggable = ({ - id, - data = {}, - disabled = false, - activationDelay = 0, - activationTolerance = Infinity, -}: UseDraggableOptions) => { - const { - containerRef, - draggableLayouts, - draggableOffsets, - draggableRestingOffsets, - draggableOptions, - draggableStates, - draggableActiveId, - draggablePendingId, - panGestureState, - } = useDndContext(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const [node, setNodeRef] = useNodeRef(); - // const key = useUniqueId("Draggable"); - // eslint-disable-next-line react-hooks/rules-of-hooks - const sharedData = isReanimatedSharedValue(data) ? data : useLatestSharedValue(data); - - const layout = useSharedValue({ - x: 0, - y: 0, - width: 0, - height: 0, - }); - const offset = useSharedPoint(0, 0); - const restingOffset = useSharedPoint(0, 0); - const state = useSharedValue('resting'); - - // Register early to allow proper referencing in useDraggableStyle - draggableStates.value[id] = state; - - useLayoutEffect(() => { - const runLayoutEffect = () => { - 'worklet'; - draggableLayouts.modify(prev => { - const newValue = { ...prev, [id]: layout }; - return newValue; - }); - draggableOffsets.modify(prev => { - const newValue = { ...prev, [id]: offset }; - return newValue; - }); - draggableRestingOffsets.modify(prev => { - const newValue = { ...prev, [id]: restingOffset }; - return newValue; - }); - draggableOptions.modify(prev => { - const newValue = { ...prev, [id]: { id, data: sharedData, disabled, activationDelay, activationTolerance } }; - return newValue; - }); - draggableStates.modify(prev => { - const newValue = { ...prev, [id]: state }; - return newValue; - }); - }; - runOnUI(runLayoutEffect)(); - - return () => { - const cleanupLayoutEffect = () => { - 'worklet'; - draggableLayouts.modify(value => { - delete value[id]; - return value; - }); - draggableOffsets.modify(value => { - delete value[id]; - return value; - }); - draggableRestingOffsets.modify(value => { - delete value[id]; - return value; - }); - draggableOptions.modify(value => { - delete value[id]; - return value; - }); - draggableStates.modify(value => { - delete value[id]; - return value; - }); - }; - runOnUI(cleanupLayoutEffect)(); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [id]); - - // Standard onLayout event for Android — also required to trigger 'topLayout' event on iOS - const onLayout: ViewProps['onLayout'] = useCallback(() => { - if (IS_IOS) return; - - assert(containerRef.current); - node.current?.measureLayout(containerRef.current, (x, y, width, height) => { - layout.modify(value => { - 'worklet'; - value.x = x; - value.y = y; - value.width = width; - value.height = height; - return value; - }); - }); - }, [containerRef, node, layout]); - - // Worklet-based onLayout event for iOS - const onLayoutWorklet = useLayoutWorklet(layoutInfo => { - 'worklet'; - - layout.modify(value => { - value.x = layoutInfo.x; - value.y = layoutInfo.y; - value.width = layoutInfo.width; - value.height = layoutInfo.height; - return value; - }); - }); - - return { - offset, - state, - setNodeRef, - activeId: draggableActiveId, - pendingId: draggablePendingId, - onLayout, - onLayoutWorklet, - panGestureState, - }; -}; diff --git a/src/components/drag-and-drop/hooks/useDraggableActiveId.ts b/src/components/drag-and-drop/hooks/useDraggableActiveId.ts deleted file mode 100644 index e40b948ce5f..00000000000 --- a/src/components/drag-and-drop/hooks/useDraggableActiveId.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { useState } from 'react'; -import { runOnJS, useAnimatedReaction } from 'react-native-reanimated'; -import { useDndContext } from '../DndContext'; -import type { UniqueIdentifier } from '../types'; - -export const useDraggableActiveId = () => { - const [activeId, setActiveId] = useState(null); - const { draggableActiveId } = useDndContext(); - useAnimatedReaction( - () => draggableActiveId.value, - (next, prev) => { - if (next !== prev) { - runOnJS(setActiveId)(next); - } - }, - [] - ); - return activeId; -}; diff --git a/src/components/drag-and-drop/hooks/useDraggableStyle.tsx b/src/components/drag-and-drop/hooks/useDraggableStyle.tsx deleted file mode 100644 index 6d610c9aa15..00000000000 --- a/src/components/drag-and-drop/hooks/useDraggableStyle.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { useAnimatedStyle } from 'react-native-reanimated'; -import { useDndContext } from '..'; -import type { AnimatedStyle, UniqueIdentifier } from '../types'; - -export type UseDraggableStyleCallback = (_: { - isActive: boolean; - isDisabled: boolean; - isActing: boolean; -}) => StyleT; - -export const useDraggableStyle = ( - id: UniqueIdentifier, - callback: UseDraggableStyleCallback -): StyleT => { - const { draggableStates: states, draggableActiveId: activeId, draggableOptions: options } = useDndContext(); - const state = states.value[id]; - return useAnimatedStyle(() => { - const isActive = activeId.value === id; - const isActing = state?.value === 'acting'; - const isDisabled = !options.value[id]?.disabled; - return callback({ isActive, isActing, isDisabled }); - }, [id, state]); -}; diff --git a/src/components/drag-and-drop/hooks/useDroppable.ts b/src/components/drag-and-drop/hooks/useDroppable.ts deleted file mode 100644 index 803104d4329..00000000000 --- a/src/components/drag-and-drop/hooks/useDroppable.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { useCallback, useLayoutEffect } from 'react'; -import { ViewProps, type LayoutRectangle } from 'react-native'; -import { runOnUI, useAnimatedReaction, useSharedValue } from 'react-native-reanimated'; -import { IS_IOS } from '@/env'; -import { useLayoutWorklet } from '@/hooks/reanimated/useLayoutWorklet'; -import { useDndContext } from '../DndContext'; -import { useLatestSharedValue, useNodeRef } from '../hooks'; -import type { Data, NativeElement, UniqueIdentifier } from '../types'; -import { assert, isReanimatedSharedValue } from '../utils'; - -export type UseDroppableOptions = { id: UniqueIdentifier; data?: Data; disabled?: boolean }; - -/** - * useDroppable is a custom hook that provides functionality for making a component droppable within a drag and drop context. - * - * @function - * @example - * const { setNodeRef, setNodeLayout, activeId, panGestureState } = useDroppable({ id: 'droppable-1' }); - * - * @param {object} options - The options that define the behavior of the droppable component. - * @param {string} options.id - A unique identifier for the droppable component. - * @param {object} [options.data={}] - Optional data associated with the droppable component. - * @param {boolean} [options.disabled=false] - A flag that indicates whether the droppable component is disabled. - * - * @returns {object} Returns an object with properties and methods related to the droppable component. - * @property {Function} setNodeRef - A function that can be used to set the ref of the droppable component. - * @property {Function} setNodeLayout - A function that handles the layout event of the droppable component. - * @property {string} activeId - The unique identifier of the currently active droppable component. - * @property {object} panGestureState - An object representing the current state of the draggable component within the context. - */ -export const useDroppable = ({ id, data = {}, disabled = false }: UseDroppableOptions) => { - const { droppableLayouts, droppableOptions, droppableActiveId, containerRef, panGestureState } = useDndContext(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const [node, setNodeRef] = useNodeRef(); - // eslint-disable-next-line react-hooks/rules-of-hooks - const sharedData = isReanimatedSharedValue(data) ? data : useLatestSharedValue(data); - - const layout = useSharedValue({ - x: 0, - y: 0, - width: 0, - height: 0, - }); - - useAnimatedReaction( - () => disabled, - (next, prev) => { - if (next !== prev) { - droppableOptions.value[id].disabled = disabled; - } - }, - [disabled] - ); - - useLayoutEffect(() => { - const runLayoutEffect = () => { - 'worklet'; - // droppableLayouts.value[id] = layout; - // droppableOptions.value[id] = { id, data: sharedData, disabled }; - - droppableLayouts.modify(value => { - const newValue = { ...value, [id]: layout }; - return newValue; - }); - droppableOptions.modify(value => { - const newValue = { ...value, [id]: { id, data: sharedData, disabled } }; - return newValue; - }); - }; - runOnUI(runLayoutEffect)(); - return () => { - const runLayoutEffect = () => { - 'worklet'; - droppableLayouts.modify(value => { - delete value[id]; - return value; - }); - droppableOptions.modify(value => { - delete value[id]; - return value; - }); - }; - // if(node && node.key === key) - runOnUI(runLayoutEffect)(); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - // Standard onLayout event for Android — also required to trigger 'topLayout' event on iOS - const onLayout: ViewProps['onLayout'] = useCallback(() => { - if (IS_IOS) return; - - assert(containerRef.current); - node.current?.measureLayout(containerRef.current, (x, y, width, height) => { - layout.modify(value => { - 'worklet'; - value.x = x; - value.y = y; - value.width = width; - value.height = height; - return value; - }); - }); - }, [containerRef, node, layout]); - - // Worklet-based onLayout event for iOS - const onLayoutWorklet = useLayoutWorklet(layoutInfo => { - 'worklet'; - - layout.modify(value => { - value.x = layoutInfo.x; - value.y = layoutInfo.y; - value.width = layoutInfo.width; - value.height = layoutInfo.height; - return value; - }); - }); - - return { setNodeRef, onLayout, onLayoutWorklet, activeId: droppableActiveId, panGestureState }; -}; diff --git a/src/components/drag-and-drop/hooks/useDroppableStyle.tsx b/src/components/drag-and-drop/hooks/useDroppableStyle.tsx deleted file mode 100644 index 3d7c6c4c3fd..00000000000 --- a/src/components/drag-and-drop/hooks/useDroppableStyle.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { useAnimatedStyle } from 'react-native-reanimated'; -import { useDndContext } from '..'; -import type { AnimatedStyle, UniqueIdentifier } from '../types'; - -export type UseDroppableStyleCallback = (_: { isActive: boolean; isDisabled: boolean }) => StyleT; - -export const useDroppableStyle = ( - id: UniqueIdentifier, - callback: UseDroppableStyleCallback -): StyleT => { - const { droppableActiveId: activeId, droppableOptions: options } = useDndContext(); - return useAnimatedStyle(() => { - const isActive = activeId.value === id; - const isDisabled = !options.value[id]?.disabled; - return callback({ isActive, isDisabled }); - }, []); -}; diff --git a/src/components/drag-and-drop/hooks/useEvent.ts b/src/components/drag-and-drop/hooks/useEvent.ts deleted file mode 100644 index 8e4615a6b72..00000000000 --- a/src/components/drag-and-drop/hooks/useEvent.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { useCallback, useLayoutEffect, useRef } from 'react'; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type EventHandler = (...args: any[]) => void; - -/** - * Hook to define an event handler with a function identity that is always stable - * {@link https://blog.logrocket.com/what-you-need-know-react-useevent-hook-rfc/} - */ -export const useEvent = (handler: T | undefined) => { - const handlerRef = useRef(handler); - - useLayoutEffect(() => { - handlerRef.current = handler; - }); - - return useCallback((...args: unknown[]) => { - return handlerRef.current?.(...args); - }, []); -}; diff --git a/src/components/drag-and-drop/hooks/useLatestSharedValue.ts b/src/components/drag-and-drop/hooks/useLatestSharedValue.ts deleted file mode 100644 index 20d506fd149..00000000000 --- a/src/components/drag-and-drop/hooks/useLatestSharedValue.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { useAnimatedReaction, useSharedValue } from 'react-native-reanimated'; -import type { DependencyList } from '../types'; - -export function useLatestSharedValue(value: T, dependencies: DependencyList = [value]) { - const sharedValue = useSharedValue(value); - - useAnimatedReaction( - () => value, - (next, prev) => { - // Ignore initial reaction - if (prev === null) { - return; - } - sharedValue.value = next; - }, - dependencies - ); - - return sharedValue; -} diff --git a/src/components/drag-and-drop/hooks/useLatestValue.ts b/src/components/drag-and-drop/hooks/useLatestValue.ts deleted file mode 100644 index 1d9f8869117..00000000000 --- a/src/components/drag-and-drop/hooks/useLatestValue.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { DependencyList, useLayoutEffect, useRef } from 'react'; - -export function useLatestValue(value: T, dependencies: DependencyList = [value]) { - const valueRef = useRef(value); - - useLayoutEffect(() => { - if (valueRef.current !== value) { - valueRef.current = value; - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, dependencies); - - return valueRef; -} diff --git a/src/components/drag-and-drop/hooks/useNodeRef.ts b/src/components/drag-and-drop/hooks/useNodeRef.ts deleted file mode 100644 index 07bdf66dd5d..00000000000 --- a/src/components/drag-and-drop/hooks/useNodeRef.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { useCallback, useRef } from 'react'; -import { useEvent } from './useEvent'; - -type NodeChangeHandler = (nextElement: T | null, prevElement: T | null) => void; - -/** - * Hook to receive a stable ref setter with an optional onChange handler - */ -export const useNodeRef = (onChange?: NodeChangeHandler) => { - const onChangeHandler = useEvent(onChange); - const nodeRef = useRef(null); - const setNodeRef = useCallback( - (element: U | null) => { - if (element !== nodeRef.current) { - onChangeHandler?.(element, nodeRef.current); - } - nodeRef.current = element as T; - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [] - ); - - return [nodeRef, setNodeRef] as const; -}; diff --git a/src/components/drag-and-drop/hooks/useSharedPoint.ts b/src/components/drag-and-drop/hooks/useSharedPoint.ts deleted file mode 100644 index 0335c41ad61..00000000000 --- a/src/components/drag-and-drop/hooks/useSharedPoint.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { useSharedValue, type SharedValue } from 'react-native-reanimated'; -import type { Point } from '../utils'; - -export type SharedPoint = Point>; - -export const useSharedPoint = (x: number, y: number): SharedPoint => { - return { - x: useSharedValue(x), - y: useSharedValue(y), - }; -}; diff --git a/src/components/drag-and-drop/hooks/useSharedValuePair.ts b/src/components/drag-and-drop/hooks/useSharedValuePair.ts deleted file mode 100644 index a921b8059fd..00000000000 --- a/src/components/drag-and-drop/hooks/useSharedValuePair.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { useSharedValue, type SharedValue } from 'react-native-reanimated'; - -export type SharedValues> = { - [K in keyof T]: SharedValue; -}; - -export const useSharedValuePair = (x: number, y: number) => { - return [useSharedValue(x), useSharedValue(y)]; -}; diff --git a/src/components/drag-and-drop/index.ts b/src/components/drag-and-drop/index.ts deleted file mode 100644 index 3ed5a7f1cfa..00000000000 --- a/src/components/drag-and-drop/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export * from './DndContext'; -export * from './DndProvider'; -export * from './components'; -export * from './features'; -export * from './hooks'; -export * from './types'; -export * from './utils'; diff --git a/src/components/drag-and-drop/types/common.ts b/src/components/drag-and-drop/types/common.ts deleted file mode 100644 index 455f5bbc0d8..00000000000 --- a/src/components/drag-and-drop/types/common.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { HostComponent, ViewProps, ViewStyle } from 'react-native'; -import type { SharedValue, useAnimatedStyle } from 'react-native-reanimated'; - -export type UniqueIdentifier = string | number; -export type ObjectWithId = { id: UniqueIdentifier; [s: string]: unknown }; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export type AnyData = Record; -export type Data = T | SharedValue; -export type SharedData = SharedValue; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export type NativeElement = InstanceType>; - -export type AnimatedStyle = ReturnType; -export type AnimatedViewStyle = ReturnType>; -export type AnimatedStyleWorklet = ( - style: Readonly, - options: { isActive: boolean; isDisabled: boolean; isActing?: boolean } -) => T; diff --git a/src/components/drag-and-drop/types/index.ts b/src/components/drag-and-drop/types/index.ts deleted file mode 100644 index 251629bf35e..00000000000 --- a/src/components/drag-and-drop/types/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './common'; -export * from './reanimated'; diff --git a/src/components/drag-and-drop/types/reanimated.ts b/src/components/drag-and-drop/types/reanimated.ts deleted file mode 100644 index 6c14f0cba3d..00000000000 --- a/src/components/drag-and-drop/types/reanimated.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { useAnimatedReaction } from 'react-native-reanimated'; - -export type DependencyList = Parameters[2]; diff --git a/src/components/drag-and-drop/utils/array.ts b/src/components/drag-and-drop/utils/array.ts deleted file mode 100644 index de114bd58c8..00000000000 --- a/src/components/drag-and-drop/utils/array.ts +++ /dev/null @@ -1,17 +0,0 @@ -export const arraysEqual = (a: unknown[], b: unknown[]): boolean => { - 'worklet'; - if (a === b) { - return true; - } - if (a.length !== b.length) { - return false; - } - - for (let i = 0; i < a.length; i++) { - if (a[i] !== b[i]) { - return false; - } - } - - return true; -}; diff --git a/src/components/drag-and-drop/utils/assert.ts b/src/components/drag-and-drop/utils/assert.ts deleted file mode 100644 index 9f290f53f05..00000000000 --- a/src/components/drag-and-drop/utils/assert.ts +++ /dev/null @@ -1,20 +0,0 @@ -class AssertionError extends Error { - name = 'AssertionError'; - code = 'ERR_ASSERTION'; - constructor( - // eslint-disable-next-line default-param-last - message = '', - public actual: unknown, - public expected: unknown = 'true', - public operator = '==' - ) { - super(message || `${actual} ${operator} ${expected}`); - Object.setPrototypeOf(this, new.target.prototype); - } -} - -export const assert: (value: unknown, message?: string) => asserts value = (value, message) => { - if (value === undefined || value === null) { - throw new AssertionError(message, value); - } -}; diff --git a/src/components/drag-and-drop/utils/geometry.ts b/src/components/drag-and-drop/utils/geometry.ts deleted file mode 100644 index 6f017685a10..00000000000 --- a/src/components/drag-and-drop/utils/geometry.ts +++ /dev/null @@ -1,124 +0,0 @@ -export type Point = { - x: T; - y: T; -}; - -export type Offset = { - x: number; - y: number; -}; - -export type Rectangle = { - x: number; - y: number; - width: number; - height: number; -}; - -/** - * @summary Split a `Rectangle` in two - * @worklet - */ -export const splitLayout = (layout: Rectangle, axis: 'x' | 'y') => { - 'worklet'; - const { x, y, width, height } = layout; - if (axis === 'x') { - return [ - { x, y, width: width / 2, height }, - { x: x + width / 2, y, width: width / 2, height }, - ]; - } - return [ - { x, y, width, height: height / 2 }, - { x, y: y + height / 2, width, height: height / 2 }, - ]; -}; - -/** - * @summary Checks if a `Point` is included inside a `Rectangle` - * @worklet - */ -export const includesPoint = (layout: Rectangle, { x, y }: Point, strict?: boolean) => { - 'worklet'; - if (strict) { - return layout.x < x && x < layout.x + layout.width && layout.y < y && y < layout.y + layout.height; - } - return layout.x <= x && x <= layout.x + layout.width && layout.y <= y && y <= layout.y + layout.height; -}; - -/** - * @summary Checks if a `Rectangle` overlaps with another `Rectangle` - * @worklet - */ -export const overlapsRectangle = (layout: Rectangle, other: Rectangle) => { - 'worklet'; - return ( - layout.x < other.x + other.width && - layout.x + layout.width > other.x && - layout.y < other.y + other.height && - layout.y + layout.height > other.y - ); -}; - -/** - * @summary Checks if a `Rectange` overlaps with another `Rectangle` with a margin - * @worklet - */ -export const overlapsRectangleBy = (layout: Rectangle, other: Rectangle, by: number) => { - 'worklet'; - return ( - layout.x < other.x + other.width - by && - layout.x + layout.width > other.x + by && - layout.y < other.y + other.height - by && - layout.y + layout.height > other.y + by - ); -}; - -/** - * @summary Apply an offset to a layout - * @worklet - */ -export const applyOffset = (layout: Rectangle, { x, y }: Offset): Rectangle => { - 'worklet'; - return { - width: layout.width, - height: layout.height, - x: layout.x + x, - y: layout.y + y, - }; -}; - -/** - * @summary Compute a center point - * @worklet - */ -export const centerPoint = (layout: Rectangle): Point => { - 'worklet'; - return { - x: layout.x + layout.width / 2, - y: layout.y + layout.height / 2, - }; -}; - -/** - * @summary Compute a center axis - * @worklet - */ -export const centerAxis = (layout: Rectangle, horizontal: boolean): number => { - 'worklet'; - return horizontal ? layout.x + layout.width / 2 : layout.y + layout.height / 2; -}; - -/** - * @summary Checks if a `Rectangle` overlaps with an axis - * @worklet - */ -export const overlapsAxis = (layout: Rectangle, axis: number, horizontal: boolean) => { - 'worklet'; - return horizontal ? layout.x < axis && layout.x + layout.width > axis : layout.y < axis && layout.y + layout.height > axis; -}; - -export const getDistance = (x: number, y: number): number => { - 'worklet'; - return Math.sqrt(Math.abs(x) ** 2 + Math.abs(y) ** 2); -}; diff --git a/src/components/drag-and-drop/utils/index.ts b/src/components/drag-and-drop/utils/index.ts deleted file mode 100644 index 513e2860213..00000000000 --- a/src/components/drag-and-drop/utils/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * from './array'; -export * from './assert'; -export * from './geometry'; -export * from './random'; -export * from './reanimated'; -export * from './swap'; diff --git a/src/components/drag-and-drop/utils/random.ts b/src/components/drag-and-drop/utils/random.ts deleted file mode 100644 index 54baa9f23b4..00000000000 --- a/src/components/drag-and-drop/utils/random.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Returns a random integer between min (inclusive) and max (inclusive). - * The value is no lower than min (or the next integer greater than min - * if min isn't an integer) and no greater than max (or the next integer - * lower than max if max isn't an integer). - * Using Math.round() will give you a non-uniform distribution! - */ -export const getRandomInt = (min: number, max: number): number => { - 'worklet'; - // eslint-disable-next-line no-param-reassign - min = Math.ceil(min); - // eslint-disable-next-line no-param-reassign - max = Math.floor(max); - return Math.floor(Math.random() * (max - min + 1)) + min; -}; diff --git a/src/components/drag-and-drop/utils/reanimated.ts b/src/components/drag-and-drop/utils/reanimated.ts deleted file mode 100644 index 6c1b59bfce9..00000000000 --- a/src/components/drag-and-drop/utils/reanimated.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { LayoutRectangle } from 'react-native'; -import { SharedValue, withSpring, type AnimatableValue, type AnimationCallback, type WithSpringConfig } from 'react-native-reanimated'; -import type { SharedPoint } from '../hooks'; -import type { AnyData } from '../types'; - -export const DND_DEFAULT_SPRING_CONFIG: WithSpringConfig = { - damping: 10, // Defines how the spring’s motion should be damped due to the forces of friction. Default 10. - mass: 1, // The mass of the object attached to the end of the spring. Default 1. - stiffness: 100, // The spring stiffness coefficient. Default 100. - overshootClamping: false, // Indicates whether the spring should be clamped and not bounce. Default false. - restSpeedThreshold: 0.001, // The speed at which the spring should be considered at rest in pixels per second. Default 0.001. - restDisplacementThreshold: 0.2, // The threshold of displacement from rest below which the spring should be considered at rest. Default 0.001. -}; -export const DND_FAST_SPRING_CONFIG: WithSpringConfig = { - damping: 20, // Defines how the spring’s motion should be damped due to the forces of friction. Default 10. - mass: 0.5, // The mass of the object attached to the end of the spring. Default 1. - stiffness: 100, // The spring stiffness coefficient. Default 100. - overshootClamping: false, // Indicates whether the spring should be clamped and not bounce. Default false. - restSpeedThreshold: 0.2, // The speed at which the spring should be considered at rest in pixels per second. Default 0.001. - restDisplacementThreshold: 0.2, // The threshold of displacement from rest below which the spring should be considered at rest. Default 0.001. -}; -export const DND_DEFAULT_SPRING_CONFIG_3: WithSpringConfig = { - damping: 20, // Defines how the spring’s motion should be damped due to the forces of friction. Default 10. - mass: 0.5, // The mass of the object attached to the end of the spring. Default 1. - stiffness: 100, // The spring stiffness coefficient. Default 100. - overshootClamping: false, // Indicates whether the spring should be clamped and not bounce. Default false. - restSpeedThreshold: 0.01, // The speed at which the spring should be considered at rest in pixels per second. Default 0.001. - restDisplacementThreshold: 0.2, // The threshold of displacement from rest below which the spring should be considered at rest. Default 0.001. -}; -export const DND_SLOW_SPRING_CONFIG: WithSpringConfig = { - damping: 20, // Defines how the spring’s motion should be damped due to the forces of friction. Default 10. - mass: 1, // The mass of the object attached to the end of the spring. Default 1. - stiffness: 10, // The spring stiffness coefficient. Default 100. - overshootClamping: false, // Indicates whether the spring should be clamped and not bounce. Default false. - restSpeedThreshold: 0.01, // The speed at which the spring should be considered at rest in pixels per second. Default 0.001. - restDisplacementThreshold: 0.2, // The threshold of displacement from rest below which the spring should be considered at rest. Default 0.001. -}; - -/** - * @summary Waits for n-callbacks - * @worklet - */ -export const waitForAll = (callback: (...args: T) => void, count = 2) => { - 'worklet'; - const status = new Array(count).fill(false); - const result = new Array(count).fill(undefined); - return status.map((_v, index) => { - return (...args: unknown[]) => { - status[index] = true; - result[index] = args; - if (status.every(Boolean)) { - callback(...(result as T)); - } - }; - }); -}; - -type AnimationCallbackParams = Parameters; - -export type AnimationPointCallback = ( - finished: [boolean | undefined, boolean | undefined], - current: [AnimatableValue | undefined, AnimatableValue | undefined] -) => void; - -// eslint-disable-next-line default-param-last -export const withDefaultSpring: typeof withSpring = (toValue, userConfig: WithSpringConfig = {}, callback) => { - 'worklet'; - // eslint-disable-next-line prefer-object-spread - const config: WithSpringConfig = Object.assign({}, DND_SLOW_SPRING_CONFIG, userConfig); - return withSpring(toValue, config, callback); -}; - -/** - * @summary Easily animate a `SharePoint` - * @worklet - */ -export const animatePointWithSpring = ( - point: SharedPoint, - [toValueX, toValueY]: [number, number], - // eslint-disable-next-line default-param-last - [configX, configY]: [WithSpringConfig | undefined, WithSpringConfig | undefined] = [undefined, undefined], - callback?: AnimationPointCallback -) => { - 'worklet'; - const [waitForX, waitForY] = waitForAll<[AnimationCallbackParams, AnimationCallbackParams]>( - ([finishedX, currentX], [finishedY, currentY]) => { - if (!callback) { - return; - } - callback([finishedX, finishedY], [currentX, currentY]); - } - ); - point.x.value = withSpring(toValueX, configX, waitForX); - point.y.value = withSpring(toValueY, configY, waitForY); -}; - -export const moveArrayIndex = (input: T[], from: number, to: number) => { - 'worklet'; - const output = input.slice(); - output.splice(to, 0, output.splice(from, 1)[0]); - return output; -}; - -export const stringifySharedPoint = ({ x, y }: SharedPoint) => { - 'worklet'; - return `{"x": ${Math.floor(x.value)}, "y": ${Math.floor(y.value)}}`; -}; - -export const stringifyLayout = ({ x, y, width, height }: LayoutRectangle) => { - 'worklet'; - return `{"x": ${Math.floor(x)}, "y": ${Math.floor(y)}, "width": ${Math.floor(width)}, "height": ${Math.floor(height)}}`; -}; - -export const floorLayout = ({ x, y, width, height }: LayoutRectangle) => { - 'worklet'; - return { - x: Math.floor(x), - y: Math.floor(y), - width: Math.floor(width), - height: Math.floor(height), - }; -}; - -/** - * @summary Checks if a value is a `Reanimated` shared value - * @param {object} value - The value to check - * @returns {boolean} Whether the value is a `Reanimated` shared value - */ -export const isReanimatedSharedValue = (value: unknown): value is SharedValue => - typeof value === 'object' && (value as { _isReanimatedSharedValue: boolean })?._isReanimatedSharedValue; diff --git a/src/components/drag-and-drop/utils/swap.ts b/src/components/drag-and-drop/utils/swap.ts deleted file mode 100644 index 5290df09dbb..00000000000 --- a/src/components/drag-and-drop/utils/swap.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { centerAxis, centerPoint, includesPoint, overlapsAxis, type Rectangle } from './geometry'; - -export const swapByItemCenterPoint = (activeLayout: Rectangle, itemLayout: Rectangle) => { - 'worklet'; - const itemCenterPoint = centerPoint(itemLayout); - return includesPoint(activeLayout, itemCenterPoint); -}; - -export const swapByItemAxis = (activeLayout: Rectangle, itemLayout: Rectangle, horizontal: boolean) => { - 'worklet'; - const itemCenterAxis = centerAxis(itemLayout, horizontal); - return overlapsAxis(activeLayout, itemCenterAxis, horizontal); -}; - -export const swapByItemHorizontalAxis = (activeLayout: Rectangle, itemLayout: Rectangle) => { - 'worklet'; - const itemCenterAxis = centerAxis(itemLayout, true); - return overlapsAxis(activeLayout, itemCenterAxis, true); -}; - -export const swapByItemVerticalAxis = (activeLayout: Rectangle, itemLayout: Rectangle) => { - 'worklet'; - const itemCenterAxis = centerAxis(itemLayout, false); - return overlapsAxis(activeLayout, itemCenterAxis, false); -}; diff --git a/src/components/easing-gradient/EasingGradient.tsx b/src/components/easing-gradient/EasingGradient.tsx deleted file mode 100644 index 0cddb0157b9..00000000000 --- a/src/components/easing-gradient/EasingGradient.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import React, { memo } from 'react'; -import { ViewProps } from 'react-native'; -import LinearGradient from 'react-native-linear-gradient'; -import { useEasingGradient, UseEasingGradientParams } from '@/hooks/useEasingGradient'; - -interface EasingGradientProps extends UseEasingGradientParams, ViewProps {} - -/** - * ### EasingGradient - * - * Renders a linear gradient with easing applied to the color transitions. - * - * **Required:** - * @param endColor The color at the end of the gradient. - * @param startColor The color at the start of the gradient. - * - * **Optional:** - * @param easing The easing function to apply to the gradient. - * @param endOpacity The opacity at the end of the gradient. - * @param endPosition The end position of the gradient ('top', 'bottom', 'left', 'right'). - * @param startOpacity The opacity at the start of the gradient. - * @param startPosition The start position of the gradient ('top', 'bottom', 'left', 'right'). Defaults to 'top'. - * @param steps The number of color steps in the gradient. Defaults to 16. - * @param props Additional ViewProps to apply to the LinearGradient component. - * - * @returns A LinearGradient component with the specified easing and color properties. - * - * @example - * ```tsx - * - * ``` - */ -export const EasingGradient = memo(function EasingGradient({ - easing, - endColor, - endOpacity, - endPosition, - startColor, - startOpacity, - startPosition = 'top', - steps = 16, - ...props -}: EasingGradientProps) { - const { colors, end, locations, start } = useEasingGradient({ - easing, - endColor, - endOpacity, - endPosition, - startColor, - startOpacity, - startPosition, - steps, - }); - - // eslint-disable-next-line react/jsx-props-no-spreading - return ; -}); diff --git a/src/hooks/reanimated/useAnimatedTimeout.ts b/src/hooks/reanimated/useAnimatedTimeout.ts index c075662a09b..04704dedc7c 100644 --- a/src/hooks/reanimated/useAnimatedTimeout.ts +++ b/src/hooks/reanimated/useAnimatedTimeout.ts @@ -20,7 +20,6 @@ interface TimeoutConfig { * - `onTimeoutWorklet` - The worklet function to be executed when the timeout completes. * * @returns An object containing: - * - `clearTimeout` - A function to clear the timeout. * - `start` - A function to initiate the timeout. * * @example @@ -39,11 +38,11 @@ interface TimeoutConfig { export function useAnimatedTimeout(config: TimeoutConfig) { const { autoStart, delayMs, onTimeoutWorklet } = config; - const { start, stop: clearTimeout } = useAnimatedTime({ + const { start } = useAnimatedTime({ autoStart, durationMs: delayMs, onEndWorklet: onTimeoutWorklet, }); - return { clearTimeout, start }; + return { start }; } diff --git a/src/hooks/reanimated/useLayoutWorklet.ts b/src/hooks/reanimated/useLayoutWorklet.ts deleted file mode 100644 index 84d2efb81be..00000000000 --- a/src/hooks/reanimated/useLayoutWorklet.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { useEvent } from 'react-native-reanimated'; -import { WorkletFunction } from 'react-native-reanimated/lib/typescript/commonTypes'; -import { IS_IOS } from '@/env'; - -interface Layout { - x: number; - y: number; - width: number; - height: number; -} - -const SHOULD_REBUILD = IS_IOS ? undefined : false; - -/** - * ### `📐 useLayoutWorklet 📐` - * @warning This hook is experimental and currently only works on iOS. - * - * Allows reacting to `onLayout` events directly from the UI thread. - * - * Meant to be used with ``. - * - * @param worklet - A worklet function to be called when the layout changes. - * The worklet receives a {@link Layout} object containing the new layout information. - * - * @returns A worklet function that can be passed to an `Animated.View` to handle layout changes. - * - * @example - * ```tsx - * const onLayoutWorklet = useLayoutWorklet((layout) => { - * 'worklet'; - * console.log('New layout:', layout); - * }); - * - * return ( - * { - * if (IS_IOS) return; - * handleAndroidLayout(layout); - * }} - * // @ts-expect-error The name of this prop does not matter but the - * // function must be passed to a prop - * onLayoutWorklet={IS_IOS ? onLayoutWorklet : undefined} - * > - * Measure me - * - * ); - * ``` - */ - -// @ts-expect-error This overload is required by the Reanimated API -export function useLayoutWorklet(worklet: (layout: Layout) => void); -export function useLayoutWorklet(worklet: WorkletFunction) { - return useEvent( - (event: { layout: Layout }) => { - 'worklet'; - worklet(event.layout); - }, - ['topLayout'], - SHOULD_REBUILD - ); -} diff --git a/src/hooks/useEasingGradient.ts b/src/hooks/useEasingGradient.ts deleted file mode 100644 index a70a6a95b6d..00000000000 --- a/src/hooks/useEasingGradient.ts +++ /dev/null @@ -1,77 +0,0 @@ -import chroma from 'chroma-js'; -import { useMemo } from 'react'; -import { Easing, EasingFunction } from 'react-native-reanimated'; - -type PositionObject = { x: number; y: number }; -type Position = 'bottom' | 'left' | 'right' | 'top' | PositionObject; - -export interface UseEasingGradientParams { - easing?: EasingFunction; - endColor: string; - endOpacity?: number; - endPosition?: Position; - startColor: string; - startOpacity?: number; - startPosition?: Position; - steps?: number; -} - -interface GradientOutput { - colors: string[]; - end: PositionObject; - locations: number[]; - start: PositionObject; -} - -const getPositionCoordinates = (position: Position): PositionObject => { - if (typeof position === 'object') { - return position; - } - switch (position) { - case 'bottom': - return { x: 0.5, y: 1 }; - case 'left': - return { x: 0, y: 0.5 }; - case 'right': - return { x: 1, y: 0.5 }; - case 'top': - default: - return { x: 0.5, y: 0 }; - } -}; - -export const useEasingGradient = ({ - easing = Easing.inOut(Easing.sin), - endColor, - endOpacity = 1, - endPosition = 'bottom', - startColor, - startOpacity = 0, - startPosition = 'top', - steps = 16, -}: UseEasingGradientParams): GradientOutput => { - return useMemo(() => { - const colors: string[] = []; - const locations: number[] = []; - - const startColorWithOpacity = chroma(startColor).alpha(startOpacity); - const endColorWithOpacity = chroma(endColor).alpha(endOpacity); - - for (let i = 0; i <= steps; i++) { - const t = i / steps; - const easedT = easing(t); - - const interpolatedColor = chroma.mix(startColorWithOpacity, endColorWithOpacity, easedT, 'rgb'); - - colors.push(interpolatedColor.css()); - locations.push(t); - } - - return { - colors, - end: getPositionCoordinates(endPosition), - locations, - start: getPositionCoordinates(startPosition), - }; - }, [easing, endColor, endOpacity, endPosition, startColor, startOpacity, startPosition, steps]); -}; diff --git a/src/model/migrations.ts b/src/model/migrations.ts index c79715a0db9..4eb72cf0de7 100644 --- a/src/model/migrations.ts +++ b/src/model/migrations.ts @@ -37,8 +37,6 @@ import { queryClient } from '@/react-query'; import { favoritesQueryKey } from '@/resources/favorites'; import { EthereumAddress, RainbowToken } from '@/entities'; import { getUniqueId } from '@/utils/ethereumUtils'; -import { standardizeUrl, useFavoriteDappsStore } from '@/state/browser/favoriteDappsStore'; -import { useLegacyFavoriteDappsStore } from '@/state/legacyFavoriteDapps'; export default async function runMigrations() { // get current version @@ -639,35 +637,6 @@ export default async function runMigrations() { migrations.push(v18); - /** - *************** Migration v19 ****************** - * Migrates dapp browser favorites store from createStore to createRainbowStore - */ - const v19 = async () => { - const initializeLegacyStore = () => { - return new Promise(resolve => { - // Give the async legacy store a moment to initialize - setTimeout(() => { - resolve(); - }, 1000); - }); - }; - - await initializeLegacyStore(); - const legacyFavorites = useLegacyFavoriteDappsStore.getState().favoriteDapps; - - if (legacyFavorites.length > 0) { - // Re-standardize URLs to ensure they're in the correct format - for (const favorite of legacyFavorites) { - favorite.url = standardizeUrl(favorite.url); - } - useFavoriteDappsStore.setState({ favoriteDapps: legacyFavorites }); - useLegacyFavoriteDappsStore.setState({ favoriteDapps: [] }); - } - }; - - migrations.push(v19); - logger.sentry(`Migrations: ready to run migrations starting on number ${currentVersion}`); // await setMigrationVersion(17); if (migrations.length === currentVersion) { diff --git a/src/state/browser/favoriteDappsStore.ts b/src/state/browser/favoriteDappsStore.ts deleted file mode 100644 index 532e9a88b21..00000000000 --- a/src/state/browser/favoriteDappsStore.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { createRainbowStore } from '../internal/createRainbowStore'; - -export interface FavoritedSite { - name: string; - url: string; - image: string; -} - -interface FavoriteDappsStore { - favoriteDapps: FavoritedSite[]; - addFavorite: (site: FavoritedSite) => void; - getFavorites: (sort?: FavoritedSite['url'][]) => FavoritedSite[]; - getOrderedIds: () => FavoritedSite['url'][]; - removeFavorite: (url: string) => void; - isFavorite: (url: string) => boolean; - reorderFavorites: (newOrder: FavoritedSite['url'][]) => void; -} - -/** - * Strips a URL down from e.g. `https://www.rainbow.me/app/` to `rainbow.me/app`. - * @param stripPath - Optionally strip the path from the URL, leaving `rainbow.me`. - */ -export const standardizeUrl = (url: string, stripPath?: boolean) => { - let standardizedUrl = url?.trim(); - standardizedUrl = standardizedUrl?.replace(/^https?:\/\//, ''); - standardizedUrl = standardizedUrl?.replace(/^www\./, ''); - if (standardizedUrl?.endsWith('/')) { - standardizedUrl = standardizedUrl?.slice(0, -1); - } - if (standardizedUrl?.includes('?')) { - standardizedUrl = standardizedUrl?.split('?')[0]; - } - if (stripPath) { - standardizedUrl = standardizedUrl?.split('/')?.[0] || standardizedUrl; - } - return standardizedUrl; -}; - -export const useFavoriteDappsStore = createRainbowStore( - (set, get) => ({ - favoriteDapps: [], - - addFavorite: site => { - const { favoriteDapps } = get(); - const standardizedUrl = standardizeUrl(site.url); - - if (!favoriteDapps.some(dapp => dapp.url === standardizedUrl)) { - set({ favoriteDapps: [...favoriteDapps, { ...site, url: standardizedUrl }] }); - } - }, - - getFavorites: sort => { - const { favoriteDapps } = get(); - if (!sort) return favoriteDapps; - - const sortMap = new Map(sort.map((url, index) => [url, index])); - - return [...favoriteDapps].sort((a, b) => { - const indexA = sortMap.get(a.url) ?? Infinity; - const indexB = sortMap.get(b.url) ?? Infinity; - return indexA - indexB; - }); - }, - - getOrderedIds: () => get().favoriteDapps.map(dapp => dapp.url), - - isFavorite: url => { - const { favoriteDapps } = get(); - const standardizedUrl = standardizeUrl(url); - const foundMatch = favoriteDapps.some(dapp => dapp.url === standardizedUrl); - if (foundMatch) return true; - - const baseUrl = standardizeUrl(url, true); - return favoriteDapps.some(dapp => dapp.url.startsWith(baseUrl)); - }, - - removeFavorite: url => { - const { favoriteDapps } = get(); - const standardizedUrl = standardizeUrl(url); - const match = favoriteDapps.find(dapp => dapp.url === standardizedUrl); - - if (match) { - set({ favoriteDapps: favoriteDapps.filter(dapp => dapp.url !== standardizedUrl) }); - } else { - const baseUrl = standardizeUrl(url, true); - const baseUrlMatch = favoriteDapps.find(dapp => dapp.url.startsWith(baseUrl)); - if (baseUrlMatch) { - set({ favoriteDapps: favoriteDapps.filter(dapp => dapp.url !== baseUrlMatch.url) }); - } - } - }, - - reorderFavorites: newOrder => { - const { favoriteDapps } = get(); - const urlMap = new Map(favoriteDapps.map(dapp => [dapp.url, dapp])); - const reorderedFavorites = newOrder.map(url => urlMap.get(url)).filter((dapp): dapp is FavoritedSite => dapp !== undefined); - const remainingFavorites = favoriteDapps.filter(dapp => !newOrder.includes(dapp.url)); - set({ favoriteDapps: [...reorderedFavorites, ...remainingFavorites] }); - }, - }), - { - storageKey: 'browserFavorites', - version: 1, - } -); diff --git a/src/state/browser/favoriteDappsStore.test.ts b/src/state/favoriteDapps/index.test.ts similarity index 59% rename from src/state/browser/favoriteDappsStore.test.ts rename to src/state/favoriteDapps/index.test.ts index 10cd8e636a3..566645fed99 100644 --- a/src/state/browser/favoriteDappsStore.test.ts +++ b/src/state/favoriteDapps/index.test.ts @@ -1,10 +1,10 @@ -import { useFavoriteDappsStore } from './favoriteDappsStore'; +import { favoriteDappsStore } from '.'; // TODO: Fix test. skipping for now to unblock CI describe.skip('FavoriteDappsStore', () => { beforeEach(() => { // Reset the store to its initial state before each test - useFavoriteDappsStore.setState( + favoriteDappsStore.setState( { favoriteDapps: [], }, @@ -13,52 +13,52 @@ describe.skip('FavoriteDappsStore', () => { }); test('should be able to add a favorite site', () => { - const { addFavorite } = useFavoriteDappsStore.getState(); - expect(useFavoriteDappsStore.getState().favoriteDapps.length).toBe(0); + const { addFavorite } = favoriteDappsStore.getState(); + expect(favoriteDappsStore.getState().favoriteDapps.length).toBe(0); addFavorite({ name: 'Uniswap', url: 'uniswap.org', image: 'uniswap.org/favicon', }); - expect(useFavoriteDappsStore.getState().favoriteDapps.length).toBe(1); + expect(favoriteDappsStore.getState().favoriteDapps.length).toBe(1); }); test('adding a duplicate favorite site should not increase the array', () => { - const { addFavorite } = useFavoriteDappsStore.getState(); + const { addFavorite } = favoriteDappsStore.getState(); addFavorite({ name: 'Zora', url: 'zora.co', image: 'zora.png', }); - expect(useFavoriteDappsStore.getState().favoriteDapps.length).toBe(1); + expect(favoriteDappsStore.getState().favoriteDapps.length).toBe(1); addFavorite({ name: 'Zora', url: 'zora.co', image: 'zora.png', }); - expect(useFavoriteDappsStore.getState().favoriteDapps.length).toBe(1); + expect(favoriteDappsStore.getState().favoriteDapps.length).toBe(1); }); test('should be able to remove a favorite site', () => { - const { addFavorite, removeFavorite } = useFavoriteDappsStore.getState(); + const { addFavorite, removeFavorite } = favoriteDappsStore.getState(); addFavorite({ name: 'Mint.fun', url: 'mint.fun', image: 'mint.fun/favicon', }); - expect(useFavoriteDappsStore.getState().favoriteDapps.length).toBe(1); + expect(favoriteDappsStore.getState().favoriteDapps.length).toBe(1); removeFavorite('mint.fun'); - expect(useFavoriteDappsStore.getState().favoriteDapps.length).toBe(0); + expect(favoriteDappsStore.getState().favoriteDapps.length).toBe(0); }); test('removing a non-existent favorite site should do nothing', () => { - const { removeFavorite } = useFavoriteDappsStore.getState(); + const { removeFavorite } = favoriteDappsStore.getState(); removeFavorite('https://nonexistentdapp.com'); - expect(useFavoriteDappsStore.getState().favoriteDapps.length).toBe(0); + expect(favoriteDappsStore.getState().favoriteDapps.length).toBe(0); }); test('should be able to check if a site is a favorite', () => { - const { addFavorite, isFavorite } = useFavoriteDappsStore.getState(); + const { addFavorite, isFavorite } = favoriteDappsStore.getState(); addFavorite({ name: 'Uniswap', url: 'uniswap.org', diff --git a/src/state/legacyFavoriteDapps/index.ts b/src/state/favoriteDapps/index.ts similarity index 65% rename from src/state/legacyFavoriteDapps/index.ts rename to src/state/favoriteDapps/index.ts index 7808f57d452..8298eba7fb4 100644 --- a/src/state/legacyFavoriteDapps/index.ts +++ b/src/state/favoriteDapps/index.ts @@ -1,24 +1,34 @@ import create from 'zustand'; -import { standardizeUrl } from '../browser/favoriteDappsStore'; import { createStore } from '../internal/createStore'; +// need to combine types here interface Site { name: string; url: string; image: string; } -interface LegacyFavoriteDappsStore { +interface FavoriteDappsStore { favoriteDapps: Site[]; addFavorite: (site: Site) => void; removeFavorite: (url: string) => void; isFavorite: (url: string) => boolean; } -export const legacyFavoriteDappsStore = createStore( +const standardizeUrl = (url: string) => { + // Strips the URL down from e.g. "https://www.rainbow.me/app/" to "rainbow.me/app" + let standardizedUrl = url?.trim(); + standardizedUrl = standardizedUrl?.replace(/^https?:\/\//, ''); + standardizedUrl = standardizedUrl?.replace(/^www\./, ''); + if (standardizedUrl?.endsWith('/')) { + standardizedUrl = standardizedUrl?.slice(0, -1); + } + return standardizedUrl; +}; + +export const favoriteDappsStore = createStore( (set, get) => ({ favoriteDapps: [], - addFavorite: site => { const { favoriteDapps } = get(); const standardizedUrl = standardizeUrl(site.url); @@ -27,17 +37,15 @@ export const legacyFavoriteDappsStore = createStore( set({ favoriteDapps: [...favoriteDapps, { ...site, url: standardizedUrl }] }); } }, - - isFavorite: url => { + removeFavorite: url => { const { favoriteDapps } = get(); const standardizedUrl = standardizeUrl(url); - return favoriteDapps.some(dapp => dapp.url === standardizedUrl); + set({ favoriteDapps: favoriteDapps.filter(dapp => dapp.url !== standardizedUrl) }); }, - - removeFavorite: url => { + isFavorite: url => { const { favoriteDapps } = get(); const standardizedUrl = standardizeUrl(url); - set({ favoriteDapps: favoriteDapps.filter(dapp => dapp.url !== standardizedUrl) }); + return favoriteDapps.some(dapp => dapp.url === standardizedUrl); }, }), { @@ -48,4 +56,4 @@ export const legacyFavoriteDappsStore = createStore( } ); -export const useLegacyFavoriteDappsStore = create(legacyFavoriteDappsStore); +export const useFavoriteDappsStore = create(favoriteDappsStore); diff --git a/src/state/remoteCards/remoteCards.ts b/src/state/remoteCards/remoteCards.ts index bd354a10588..205c59a1cb4 100644 --- a/src/state/remoteCards/remoteCards.ts +++ b/src/state/remoteCards/remoteCards.ts @@ -7,25 +7,28 @@ import { createRainbowStore } from '@/state/internal/createRainbowStore'; export type CardKey = string; export interface RemoteCardsState { - cards: Map; cardsById: Set; - dismissCard: (id: string) => void; + cards: Map; + + setCards: (cards: TrimmedCards) => void; + getCard: (id: string) => TrimmedCard | undefined; - getCardIdsForScreen: (screen: keyof typeof Routes) => string[]; getCardPlacement: (id: string) => TrimmedCard['placement'] | undefined; - setCards: (cards: TrimmedCards) => void; + dismissCard: (id: string) => void; + + getCardIdsForScreen: (screen: keyof typeof Routes) => string[]; } type RoutesWithIndex = typeof Routes & { [key: string]: string }; -type SerializedRemoteCardsState = Omit, 'cards' | 'cardsById'> & { - cards: Array<[string, TrimmedCard]>; +type RemoteCardsStateWithTransforms = Omit, 'cards' | 'cardsById'> & { cardsById: Array; + cards: Array<[string, TrimmedCard]>; }; function serializeState(state: Partial, version?: number) { try { - const validCards = Array.from(state.cards?.entries() ?? []).filter(([, card]) => card && card.sys?.id); + const validCards = Array.from(state.cards?.entries() ?? []).filter(([, card]) => card && card.sys && card.sys.id); if (state.cards && validCards.length < state.cards.size) { logger.error(new RainbowError('remoteCardsStore: filtered cards without sys.id during serialization'), { @@ -33,10 +36,10 @@ function serializeState(state: Partial, version?: number) { }); } - const transformedStateToPersist: SerializedRemoteCardsState = { + const transformedStateToPersist: RemoteCardsStateWithTransforms = { ...state, - cards: validCards, cardsById: state.cardsById ? Array.from(state.cardsById) : [], + cards: validCards, }; return JSON.stringify({ @@ -50,7 +53,7 @@ function serializeState(state: Partial, version?: number) { } function deserializeState(serializedState: string) { - let parsedState: { state: SerializedRemoteCardsState; version: number }; + let parsedState: { state: RemoteCardsStateWithTransforms; version: number }; try { parsedState = JSON.parse(serializedState); } catch (error) { @@ -73,7 +76,7 @@ function deserializeState(serializedState: string) { let cardsData: Map = new Map(); try { if (state.cards.length) { - const validCards = state.cards.filter(([, card]) => card && card.sys?.id); + const validCards = state.cards.filter(([, card]) => card && card.sys && typeof card.sys.id === 'string'); if (validCards.length < state.cards.length) { logger.error(new RainbowError('Filtered out cards without sys.id during deserialization'), { @@ -105,10 +108,10 @@ export const remoteCardsStore = createRainbowStore( setCards: (cards: TrimmedCards) => { const cardsData = new Map(); - const validCards = Object.values(cards).filter(card => card?.sys?.id); + const validCards = Object.values(cards).filter(card => card.sys.id); validCards.forEach(card => { - const existingCard = get().getCard(card.sys.id); + const existingCard = get().getCard(card.sys.id as string); if (existingCard) { cardsData.set(card.sys.id, { ...existingCard, ...card }); } else { @@ -118,18 +121,18 @@ export const remoteCardsStore = createRainbowStore( set({ cards: cardsData, - cardsById: new Set(validCards.map(card => card.sys.id)), + cardsById: new Set(validCards.map(card => card.sys.id as string)), }); }, getCard: (id: string) => { const card = get().cards.get(id); - return card?.sys?.id ? card : undefined; + return card && card.sys.id ? card : undefined; }, getCardPlacement: (id: string) => { const card = get().getCard(id); - if (!card || !card.sys?.id || !card.placement) { + if (!card || !card.sys.id || !card.placement) { return undefined; } @@ -155,21 +158,17 @@ export const remoteCardsStore = createRainbowStore( // NOTE: This is kinda a hack to immediately dismiss the card from the carousel and not have an empty space // it will be added back during the next fetch - const newCardsById = new Set(state.cardsById); - newCardsById.delete(id); + state.cardsById.delete(id); + return { ...state, cards: new Map(state.cards.set(id, newCard)), - cardsById: newCardsById, }; }), getCardIdsForScreen: (screen: keyof typeof Routes) => { return Array.from(get().cards.values()) - .filter( - (card): card is TrimmedCard & { sys: { id: string } } => - !!card?.sys?.id && !card.dismissed && get().getCardPlacement(card.sys.id) === screen - ) + .filter(card => card.sys.id && get().getCardPlacement(card.sys.id) === screen && !card.dismissed) .sort((a, b) => { if (a.index === b.index) return 0; if (a.index === undefined || a.index === null) return 1;