Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion packages/core-mobile/app/hooks/earn/useNodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ export const useNodes = (
isDeveloperMode
)
return EarnService.getCurrentValidators(provider)
}
},
// Cache validators for 5 minutes to reduce network calls
// Note: gcTime is inherited from global config (Infinity), which keeps data in memory
// This is intentional to avoid refetching large validator arrays frequently
staleTime: 5 * 60 * 1000
})
}
74 changes: 45 additions & 29 deletions packages/core-mobile/app/hooks/earn/useSearchNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { useSelector } from 'react-redux'
import Logger from 'utils/Logger'
import { NodeValidator, NodeValidators } from 'types/earn'
import { TokenUnit } from '@avalabs/core-utils-sdk'
import { useMemo } from 'react'
import { usePeers } from './usePeers'

type useSearchNodeProps = {
Expand All @@ -26,37 +27,52 @@ export const useSearchNode = ({

const isDeveloperMode = useSelector(selectIsDeveloperMode)
const isEndTimeOverOneYear = isOverOneYear(stakingEndTime)
const noMatchError = new Error(
`no node matches filter criteria: stakingAmount: ${stakingAmount.toDisplay()}, stakingEndTime: ${stakingEndTime}, minUpTime: 98%`
const noMatchError = useMemo(
() =>
new Error(
`no node matches filter criteria: stakingAmount: ${stakingAmount.toDisplay()}, stakingEndTime: ${stakingEndTime}, minUpTime: 98%`
),
[stakingAmount, stakingEndTime]
)
const noValidatorsError = new Error(`no validators found.`)
const noValidatorsError = useMemo(() => new Error(`no validators found.`), [])

if (validators && validators.length > 0) {
const filteredValidators = getFilteredValidators({
isDeveloperMode,
validators,
stakingAmount,
stakingEndTime,
minUpTime: 98,
maxFee: 4,
isEndTimeOverOneYear
})
return useMemo(() => {
if (validators && validators.length > 0) {
const filteredValidators = getFilteredValidators({
isDeveloperMode,
validators,
stakingAmount,
stakingEndTime,
minUpTime: 98,
maxFee: 4,
isEndTimeOverOneYear
})

if (filteredValidators.length === 0) {
Logger.info(noMatchError.message)
return { validator: undefined, error: noMatchError }
if (filteredValidators.length === 0) {
Logger.info(noMatchError.message)
return { validator: undefined, error: noMatchError }
}
const sortedValidators = getSimpleSortedValidators(
filteredValidators,
peers,
isEndTimeOverOneYear
)
const matchedValidator = getRandomValidator(
sortedValidators,
isEndTimeOverOneYear
)
return { validator: matchedValidator, error: undefined }
}
const sortedValidators = getSimpleSortedValidators(
filteredValidators,
peers,
isEndTimeOverOneYear
)
const matchedValidator = getRandomValidator(
sortedValidators,
isEndTimeOverOneYear
)
return { validator: matchedValidator, error: undefined }
}
Logger.info(noValidatorsError.message)
return { validator: undefined, error: noValidatorsError }
Logger.info(noValidatorsError.message)
return { validator: undefined, error: noValidatorsError }
}, [
validators,
isDeveloperMode,
stakingAmount,
stakingEndTime,
isEndTimeOverOneYear,
peers,
noMatchError,
noValidatorsError
])
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export function useModalScreensOptions(): {
return {
modalScreensOptions: {
...modalScreensOptions,
freezeOnBlur: true,
contentStyle: {
// Android formsheet in native-stack has a default top padding of insets.top
// by removing the insets.top this we adjust the navigation bar position
Expand All @@ -62,6 +63,7 @@ export function useModalScreensOptions(): {
},
secondaryModalScreensOptions: {
...modalScreensOptions,
freezeOnBlur: true,
contentStyle: {
// Android formsheet in native-stack has a default top padding of insets.top
// by removing the insets.top this we adjust the navigation bar position
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { View } from '@avalabs/k2-alpine'
import BlurredBackgroundView from 'common/components/BlurredBackgroundView'
import React, { useEffect, useRef, useState } from 'react'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import {
LayoutChangeEvent,
LayoutRectangle,
Expand Down Expand Up @@ -72,9 +72,9 @@ export const useFadingHeaderNavigation = ({
targetLayoutRef.current = targetLayout
}, [targetLayout])

const handleLayout = (event: LayoutChangeEvent): void => {
const handleLayout = useCallback((event: LayoutChangeEvent): void => {
setNavigationHeaderLayout(event.nativeEvent.layout)
}
}, [])

const handleScroll = (
event: NativeSyntheticEvent<NativeScrollEvent> | NativeScrollEvent | number
Expand Down Expand Up @@ -107,7 +107,6 @@ export const useFadingHeaderNavigation = ({
const headerHeight =
targetLayout?.height ?? navigationHeaderLayout?.height ?? 0

// Animated styles for header transformation
const animatedHeaderStyle = useAnimatedStyle(() => {
const translateY = interpolate(
targetHiddenProgress.value,
Expand All @@ -126,91 +125,113 @@ export const useFadingHeaderNavigation = ({
}
})

// eslint-disable-next-line sonarjs/cognitive-complexity
useFocusEffect(() => {
const navigationOptions: NativeStackNavigationOptions = {
headerBackground: () =>
hideHeaderBackground ? (
// Use a Pressable to receive gesture events for modal gestures
<Pressable style={{ flex: 1 }}>
{shouldHeaderHaveGrabber === true ? <Grabber /> : null}
</Pressable>
) : (
<BlurredBackgroundView
backgroundColor={backgroundColor}
shouldDelayBlurOniOS={shouldDelayBlurOniOS}
hasGrabber={shouldHeaderHaveGrabber}
separator={
hasSeparator
? {
position: 'bottom',
opacity: targetHiddenProgress
}
: undefined
}
/>
)
}

if (showNavigationHeaderTitle && header) {
navigationOptions.headerTitle = () => (
<View
style={[
{
justifyContent: 'center',
overflow: 'hidden'
},
Platform.OS === 'ios'
? {
paddingTop: shouldHeaderHaveGrabber ? 4 : 0,
height: '100%'
}
: {
// Hardcoded value for Android because 100% doesn't work properly
height: 56
}
]}>
<View onLayout={handleLayout}>
<Animated.View style={[animatedHeaderStyle]}>
{header}
</Animated.View>
</View>
const headerBackgroundComponent = useMemo(() => {
return hideHeaderBackground ? (
// Use a Pressable to receive gesture events for modal gestures
<Pressable style={{ flex: 1 }}>
{shouldHeaderHaveGrabber === true ? <Grabber /> : null}
</Pressable>
) : (
<BlurredBackgroundView
backgroundColor={backgroundColor}
shouldDelayBlurOniOS={shouldDelayBlurOniOS}
hasGrabber={shouldHeaderHaveGrabber}
separator={
hasSeparator
? {
position: 'bottom',
opacity: targetHiddenProgress
}
: undefined
}
/>
)
}, [
hideHeaderBackground,
shouldHeaderHaveGrabber,
backgroundColor,
shouldDelayBlurOniOS,
hasSeparator,
targetHiddenProgress
])

const headerBackground = useCallback(() => {
return headerBackgroundComponent
}, [headerBackgroundComponent])

// Memoize the header title component to prevent unnecessary re-creation
// This helps prevent the "child already has a parent" error on Android
const headerTitleComponent = useMemo(() => {
return (
<View
style={[
{
justifyContent: 'center',
overflow: 'hidden'
},
Platform.OS === 'ios'
? {
paddingTop: shouldHeaderHaveGrabber ? 4 : 0,
height: '100%'
}
: {
// Hardcoded value for Android because 100% doesn't work properly
height: 56
}
]}>
<View onLayout={handleLayout}>
<Animated.View style={[animatedHeaderStyle]}>{header}</Animated.View>
</View>
)
}
</View>
)
}, [shouldHeaderHaveGrabber, animatedHeaderStyle, header, handleLayout])

// If a custom header right component is provided, set it
if (renderHeaderRight) {
navigationOptions.headerRight = renderHeaderRight
// Return a stable function reference that returns the memoized component
const headerTitle = useCallback(() => {
return headerTitleComponent
}, [headerTitleComponent])

if (hasParent) {
navigation.getParent()?.setOptions(navigationOptions)
useFocusEffect(
useCallback(() => {
const navigationOptions: NativeStackNavigationOptions = {
headerBackground
}

// Clean up the header right component when the screen is unmounted
return () => {
navigation.getParent()?.setOptions({
headerRight: undefined
})
}
} else {
navigation.setOptions(navigationOptions)

// Clean up the header right component when the screen is unmounted
return () => {
navigation.setOptions({
headerRight: undefined
})
}
if (showNavigationHeaderTitle && header) {
navigationOptions.headerTitle = headerTitle
}
}

// Set the navigation options
if (hasParent) {
navigation.getParent()?.setOptions(navigationOptions)
} else {
navigation.setOptions(navigationOptions)
}
})
// If a custom right header component is provided, set it in the navigation options
if (renderHeaderRight) {
navigationOptions.headerRight = renderHeaderRight
}

const targetNavigation = hasParent ? navigation.getParent() : navigation

// Set the navigation options
targetNavigation?.setOptions(navigationOptions)

// Clean up all header options when the screen is unmounted or loses focus
// This prevents the "child already has a parent" error by ensuring
// old header views are properly removed before new ones are added
return () => {
const cleanupOptions: NativeStackNavigationOptions = {
headerBackground: undefined,
headerTitle: undefined,
headerRight: undefined
}
targetNavigation?.setOptions(cleanupOptions)
}
}, [
headerBackground,
headerTitle,
showNavigationHeaderTitle,
header,
renderHeaderRight,
hasParent,
navigation
])
)

return {
onScroll: handleScroll,
Expand Down
Loading
Loading