Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ const PerpsHomeView = () => {
<PerpsMarketTypeSection
title={strings('perps.home.stocks_and_commodities')}
markets={stocksAndCommoditiesMarkets}
marketType="all"
marketType="stocks_and_commodities"
sortBy={sortBy}
isLoading={isLoading.markets}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ const PerpsMarketListView = ({

const fadeAnimation = useRef(new Animated.Value(0)).current;
const tabScrollViewRef = useRef<ScrollView>(null);
const isScrollingProgrammatically = useRef(false);
const [isSortFieldSheetVisible, setIsSortFieldSheetVisible] = useState(false);
const [isStocksCommoditiesSheetVisible, setIsStocksCommoditiesSheetVisible] =
useState(false);
Expand Down Expand Up @@ -132,83 +133,20 @@ const PerpsMarketListView = ({
[onMarketSelect, perpsNavigation, route.params?.source],
);

// Get filtered markets for specific tab (used within each tab)
const getFilteredMarketsForTab = useCallback(
(filter: 'all' | 'crypto' | 'stocks_and_commodities') => {
if (searchQuery.trim()) {
// When searching, show all search results (filtering handled by search)
return filteredMarkets;
}

// Filter by tab when not searching
if (filter === 'all') {
// All = Crypto + Stocks + Commodities (excluding forex)
return filteredMarkets.filter(
(m) =>
!m.marketType ||
m.marketType === 'equity' ||
m.marketType === 'commodity',
);
}
if (filter === 'crypto') {
// Crypto markets have no marketType set
return filteredMarkets.filter((m) => !m.marketType);
}
if (filter === 'stocks_and_commodities') {
// Combined stocks and commodities filter - apply sub-filter
let stocksCommoditiesMarkets = filteredMarkets.filter(
(m) => m.marketType === 'equity' || m.marketType === 'commodity',
);

// Apply stocks/commodities sub-filter if not 'all'
if (stocksCommoditiesFilter !== 'all') {
stocksCommoditiesMarkets = stocksCommoditiesMarkets.filter(
(m) => m.marketType === stocksCommoditiesFilter,
);
}

return stocksCommoditiesMarkets;
}
return filteredMarkets;
},
[filteredMarkets, searchQuery, stocksCommoditiesFilter],
);

// Market type tab content component (filters markets by tab type)
// tabLabel is extracted by TabsList component for display, not used here
const MarketTypeTabContent = useCallback(
({
tabFilter,
tabLabel: _tabLabel,
}: {
tabFilter: 'all' | 'crypto' | 'stocks_and_commodities';
tabLabel: string;
}) => {
const tabMarkets = getFilteredMarketsForTab(tabFilter);
return (
<Animated.View
style={[styles.animatedListContainer, { opacity: fadeAnimation }]}
>
<PerpsMarketList
markets={tabMarkets}
onMarketPress={handleMarketPress}
sortBy={sortBy}
showBadge={false}
contentContainerStyle={styles.tabContentContainer}
testID={`${PerpsMarketListViewSelectorsIDs.MARKET_LIST}-${tabFilter}`}
/>
</Animated.View>
// Apply stocks/commodities sub-filter when on Stocks tab
const displayMarkets = useMemo(() => {
// If on stocks_and_commodities tab and sub-filter is active, apply it
if (
marketTypeFilter === 'stocks_and_commodities' &&
stocksCommoditiesFilter !== 'all'
) {
return filteredMarkets.filter(
(m) => m.marketType === stocksCommoditiesFilter,
);
},
[
getFilteredMarketsForTab,
handleMarketPress,
sortBy,
fadeAnimation,
styles.animatedListContainer,
styles.tabContentContainer,
],
);
}
// Otherwise, use markets already filtered by the hook
return filteredMarkets;
}, [filteredMarkets, marketTypeFilter, stocksCommoditiesFilter]);

// Build tabs data for TabsBar
const tabsData = useMemo(() => {
Expand Down Expand Up @@ -249,19 +187,6 @@ const PerpsMarketListView = ({
return tabs;
}, [marketCounts]);

// Build tab content components
const tabsToRender = useMemo(
() =>
tabsData.map((tab) => (
<MarketTypeTabContent
key={tab.key}
tabFilter={tab.filter}
tabLabel={tab.label}
/>
)),
[tabsData, MarketTypeTabContent],
);

// Calculate active tab index from current marketTypeFilter
const activeTabIndex = useMemo(() => {
if (tabsData.length === 0) {
Expand Down Expand Up @@ -294,9 +219,14 @@ const PerpsMarketListView = ({
[tabsData, setMarketTypeFilter],
);

// Handle scroll to sync active tab
// Handle scroll to sync active tab (for swipe gestures)
const handleScroll = useCallback(
(event: { nativeEvent: { contentOffset: { x: number } } }) => {
// Ignore programmatic scrolls to prevent feedback loop with useEffect
if (isScrollingProgrammatically.current) {
return;
}

const offsetX = event.nativeEvent.contentOffset.x;
const index = Math.round(offsetX / containerWidth);
if (index >= 0 && index < tabsData.length) {
Expand All @@ -309,29 +239,34 @@ const PerpsMarketListView = ({
[containerWidth, tabsData, marketTypeFilter, setMarketTypeFilter],
);

// Sync scroll position when active tab changes (e.g., from navigation param)
// Sync scroll position when active tab changes (e.g., from tab bar press or navigation param)
useEffect(() => {
if (
tabScrollViewRef.current &&
activeTabIndex >= 0 &&
tabsData.length > 0
) {
isScrollingProgrammatically.current = true;
tabScrollViewRef.current.scrollTo({
x: activeTabIndex * containerWidth,
animated: true,
});
// Clear flag after animation completes (~300ms animation + 50ms buffer)
setTimeout(() => {
isScrollingProgrammatically.current = false;
}, 350);
}
}, [activeTabIndex, containerWidth, tabsData.length]);

useEffect(() => {
if (filteredMarkets.length > 0) {
if (displayMarkets.length > 0) {
Animated.timing(fadeAnimation, {
toValue: 1,
duration: 300,
useNativeDriver: true,
}).start();
}
}, [filteredMarkets.length, fadeAnimation]);
}, [displayMarkets.length, fadeAnimation]);

// Reset stocks/commodities filter to 'all' when switching tabs
// This ensures that when switching to the Stocks tab, it always shows both stocks and commodities
Expand Down Expand Up @@ -364,15 +299,15 @@ const PerpsMarketListView = ({
// Performance tracking: Measure screen load time until market data is displayed
usePerpsMeasurement({
traceName: TraceName.PerpsMarketListView,
conditions: [filteredMarkets.length > 0],
conditions: [displayMarkets.length > 0],
});

// Track markets screen viewed event
const source =
route.params?.source || PerpsEventValues.SOURCE.MAIN_ACTION_BUTTON;
usePerpsEventTracking({
eventName: MetaMetricsEvents.PERPS_SCREEN_VIEWED,
conditions: [filteredMarkets.length > 0],
conditions: [displayMarkets.length > 0],
properties: {
[PerpsEventProperties.SCREEN_TYPE]: PerpsEventValues.SCREEN_TYPE.MARKETS,
[PerpsEventProperties.SOURCE]: source,
Expand All @@ -397,7 +332,7 @@ const PerpsMarketListView = ({
}

// Error (Failed to load markets)
if (error && filteredMarkets.length === 0) {
if (error && displayMarkets.length === 0) {
return (
<View style={styles.errorContainer}>
<Text
Expand All @@ -415,7 +350,7 @@ const PerpsMarketListView = ({
}

// Empty favorites results - show when favorites filter is active but no favorites found
if (showFavoritesOnly && filteredMarkets.length === 0) {
if (showFavoritesOnly && displayMarkets.length === 0) {
return (
<View style={styles.emptyStateContainer}>
<Icon
Expand Down Expand Up @@ -443,7 +378,7 @@ const PerpsMarketListView = ({
}

// Empty search results - show when search is visible and no markets match
if (isSearchVisible && filteredMarkets.length === 0) {
if (isSearchVisible && displayMarkets.length === 0) {
return (
<View style={styles.emptyStateContainer}>
<Icon
Expand Down Expand Up @@ -478,7 +413,7 @@ const PerpsMarketListView = ({
style={[styles.animatedListContainer, { opacity: fadeAnimation }]}
>
<PerpsMarketList
markets={filteredMarkets}
markets={displayMarkets}
onMarketPress={handleMarketPress}
sortBy={sortBy}
showBadge={false}
Expand Down Expand Up @@ -518,7 +453,7 @@ const PerpsMarketListView = ({
tabs={tabsData.map((tab) => ({
key: tab.key,
label: tab.label,
content: null, // Content is rendered separately in ScrollView
content: null,
isDisabled: false,
}))}
activeIndex={activeTabIndex}
Expand All @@ -527,7 +462,7 @@ const PerpsMarketListView = ({
/>

{/* Filter Bar - Between tabs and content */}
{(filteredMarkets.length > 0 || showFavoritesOnly) && (
{(displayMarkets.length > 0 || showFavoritesOnly) && (
<PerpsMarketFiltersBar
selectedOptionId={selectedOptionId}
onSortPress={() => setIsSortFieldSheetVisible(true)}
Expand All @@ -542,7 +477,7 @@ const PerpsMarketListView = ({
/>
)}

{/* Tab Content - Scrollable */}
{/* Tab Content - Swipeable */}
<ScrollView
ref={tabScrollViewRef}
horizontal
Expand All @@ -555,26 +490,40 @@ const PerpsMarketListView = ({
}}
style={styles.tabScrollView}
>
{tabsToRender.map((tabContent, index) => (
{tabsData.map((tab) => (
<View
key={tabsData[index]?.key || index}
key={tab.key}
style={[
styles.tabContentContainer,
{ width: containerWidth },
]}
>
{tabContent}
<Animated.View
style={[
styles.animatedListContainer,
{ opacity: fadeAnimation },
]}
>
<PerpsMarketList
markets={displayMarkets}
onMarketPress={handleMarketPress}
sortBy={sortBy}
showBadge={false}
contentContainerStyle={styles.tabContentContainer}
testID={`${PerpsMarketListViewSelectorsIDs.MARKET_LIST}-${tab.filter}`}
/>
</Animated.View>
</View>
))}
</ScrollView>
</View>
)}

{/* Market list hidden when tabs are shown (tabs contain the list) */}
{/* Market list when no tabs shown (rare case) */}
{!isSearchVisible &&
!isLoadingMarkets &&
!error &&
tabsToRender.length === 0 && (
tabsData.length === 0 && (
<View style={styles.listContainerWithTabBar}>
{renderMarketList()}
</View>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -390,21 +390,21 @@ describe('PerpsMarketRowItem', () => {
mockUsePerpsLivePrices.mockReturnValue({
BTC: { price: '50000', volume24h: 750000000 },
});
rerender(<PerpsMarketRowItem market={mockMarketData} />);
rerender(<PerpsMarketRowItem market={{ ...mockMarketData }} />);
expect(screen.getByText('$750.00M')).toBeOnTheScreen(); // M shows 2 decimals

// Test thousands (0 decimals with formatVolume)
mockUsePerpsLivePrices.mockReturnValue({
BTC: { price: '50000', volume24h: 50000 },
});
rerender(<PerpsMarketRowItem market={mockMarketData} />);
rerender(<PerpsMarketRowItem market={{ ...mockMarketData }} />);
expect(screen.getByText('$50K')).toBeOnTheScreen(); // K shows no decimals

// Test small values (2 decimals with formatVolume)
mockUsePerpsLivePrices.mockReturnValue({
BTC: { price: '50000', volume24h: 123.45 },
});
rerender(<PerpsMarketRowItem market={mockMarketData} />);
rerender(<PerpsMarketRowItem market={{ ...mockMarketData }} />);
expect(screen.getByText('$123.45')).toBeOnTheScreen(); // Shows 2 decimals
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,4 +211,4 @@ const PerpsMarketRowItem = ({
);
};

export default PerpsMarketRowItem;
export default React.memo(PerpsMarketRowItem);
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ describe('PerpsMarketTypeSection', () => {
expect(mockNavigate).toHaveBeenCalledWith(Routes.PERPS.ROOT, {
screen: Routes.PERPS.MARKET_LIST,
params: {
defaultMarketTypeFilter: 'all',
defaultMarketTypeFilter: 'crypto',
},
});
});
Expand All @@ -261,7 +261,7 @@ describe('PerpsMarketTypeSection', () => {
expect(mockNavigate).toHaveBeenCalledWith(Routes.PERPS.ROOT, {
screen: Routes.PERPS.MARKET_LIST,
params: {
defaultMarketTypeFilter: 'all',
defaultMarketTypeFilter: 'equity',
},
});
});
Expand All @@ -280,7 +280,7 @@ describe('PerpsMarketTypeSection', () => {
expect(mockNavigate).toHaveBeenCalledWith(Routes.PERPS.ROOT, {
screen: Routes.PERPS.MARKET_LIST,
params: {
defaultMarketTypeFilter: 'all',
defaultMarketTypeFilter: 'commodity',
},
});
});
Expand Down
Loading
Loading