Skip to content

Commit 18c8cec

Browse files
committed
chore: toggle update and test fix
1 parent e599eaa commit 18c8cec

5 files changed

Lines changed: 164 additions & 97 deletions

File tree

app/components/Nav/Main/MainNavigator.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1195,7 +1195,7 @@ describe('MainNavigator', () => {
11951195
});
11961196
});
11971197

1198-
it('includes TopTradersView screen when Social Leaderboard remote flag is enabled', () => {
1198+
it('includes SocialTradersView screen when Social Leaderboard remote flag is enabled', () => {
11991199
const stateWithSocialLeaderboard = {
12001200
...initialRootState,
12011201
engine: {
@@ -1244,7 +1244,7 @@ describe('MainNavigator', () => {
12441244
);
12451245

12461246
expect(topTradersScreen).toBeDefined();
1247-
expect(topTradersScreen?.component.name).toBe('TopTradersView');
1247+
expect(topTradersScreen?.component.name).toBe('SocialTradersView');
12481248
});
12491249

12501250
describe('Inner navigator component rendering', () => {

app/components/Views/SocialLeaderboard/FeedView/components/FeedAudienceToggle.tsx

Lines changed: 80 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,13 @@ import {
66
TextColor,
77
TextVariant,
88
} from '@metamask/design-system-react-native';
9-
import React, { useCallback, useEffect, useRef, useState } from 'react';
9+
import React, {
10+
startTransition,
11+
useCallback,
12+
useEffect,
13+
useRef,
14+
useState,
15+
} from 'react';
1016
import {
1117
StyleSheet,
1218
TouchableOpacity,
@@ -42,6 +48,16 @@ const styles = StyleSheet.create({
4248
bottom: 0,
4349
borderRadius: 12,
4450
},
51+
labelWrap: {
52+
position: 'relative',
53+
alignItems: 'center',
54+
justifyContent: 'center',
55+
},
56+
labelActive: {
57+
...StyleSheet.absoluteFillObject,
58+
alignItems: 'center',
59+
justifyContent: 'center',
60+
},
4561
});
4662

4763
export interface FeedAudienceToggleProps {
@@ -54,9 +70,10 @@ export interface FeedAudienceToggleProps {
5470
* Following / All segmented toggle with an animated sliding pill, modeled on
5571
* the QuickBuy Buy/Sell toggle. Fires a selection haptic on change.
5672
*
57-
* Label styling follows local `displayValue` so the active segment turns white
58-
* on tap, without waiting for the parent feed to finish re-rendering after a
59-
* scope change.
73+
* The active (white) label cross-fades in on the same `slideProgress` spring as
74+
* the pill, so the colour tracks the slide rather than snapping. The scope
75+
* change is dispatched in a transition so the toggle paints before the feed
76+
* re-renders.
6077
*/
6178
const FeedAudienceToggle: React.FC<FeedAudienceToggleProps> = ({
6279
value,
@@ -98,9 +115,16 @@ const FeedAudienceToggle: React.FC<FeedAudienceToggleProps> = ({
98115
}
99116

100117
playSelection().catch(() => undefined);
118+
// Flip the label color + slide the pill immediately so the
119+
// toggle feels responsive.
101120
setDisplayValue(next);
102121
animateSlideTo(next);
103-
onChange(next);
122+
// The scope change triggers an expensive feed re-render (new
123+
// query + skeleton). Marking it a transition lets React commit the urgent
124+
// toggle paint first instead of batching it behind the heavy work.
125+
startTransition(() => {
126+
onChange(next);
127+
});
104128
};
105129

106130
useEffect(() => {
@@ -131,6 +155,15 @@ const FeedAudienceToggle: React.FC<FeedAudienceToggleProps> = ({
131155
transform: [{ translateX: slideProgress.value * followingWidthSV.value }],
132156
}));
133157

158+
// Cross-fade the active (white) label in sync with the pill: driven by the
159+
// same spring, so the colour transition tracks the slide instead of snapping.
160+
const followingActiveStyle = useAnimatedStyle(() => ({
161+
opacity: Math.max(0, Math.min(1, 1 - slideProgress.value)),
162+
}));
163+
const allActiveStyle = useAnimatedStyle(() => ({
164+
opacity: Math.max(0, Math.min(1, slideProgress.value)),
165+
}));
166+
134167
const sliderWidth =
135168
displayValue === 'following' ? (followingLayout?.width ?? 0) : allWidth;
136169

@@ -164,21 +197,27 @@ const FeedAudienceToggle: React.FC<FeedAudienceToggleProps> = ({
164197
testID={getFeedAudienceOptionTestId('following')}
165198
>
166199
<Box twClassName="rounded-xl px-4 h-8 items-center justify-center">
167-
<Text
168-
variant={TextVariant.BodyMd}
169-
fontWeight={
170-
displayValue === 'following'
171-
? FontWeight.Medium
172-
: FontWeight.Regular
173-
}
174-
color={
175-
displayValue === 'following'
176-
? TextColor.TextDefault
177-
: TextColor.TextAlternative
178-
}
179-
>
180-
{strings('social_leaderboard.feed.following')}
181-
</Text>
200+
<Box style={styles.labelWrap}>
201+
<Text
202+
variant={TextVariant.BodyMd}
203+
fontWeight={FontWeight.Regular}
204+
color={TextColor.TextAlternative}
205+
>
206+
{strings('social_leaderboard.feed.following')}
207+
</Text>
208+
<Animated.View
209+
style={[styles.labelActive, followingActiveStyle]}
210+
pointerEvents="none"
211+
>
212+
<Text
213+
variant={TextVariant.BodyMd}
214+
fontWeight={FontWeight.Medium}
215+
color={TextColor.TextDefault}
216+
>
217+
{strings('social_leaderboard.feed.following')}
218+
</Text>
219+
</Animated.View>
220+
</Box>
182221
</Box>
183222
</TouchableOpacity>
184223

@@ -194,19 +233,27 @@ const FeedAudienceToggle: React.FC<FeedAudienceToggleProps> = ({
194233
testID={getFeedAudienceOptionTestId('all')}
195234
>
196235
<Box twClassName="rounded-xl px-4 h-8 items-center justify-center">
197-
<Text
198-
variant={TextVariant.BodyMd}
199-
fontWeight={
200-
displayValue === 'all' ? FontWeight.Medium : FontWeight.Regular
201-
}
202-
color={
203-
displayValue === 'all'
204-
? TextColor.TextDefault
205-
: TextColor.TextAlternative
206-
}
207-
>
208-
{strings('social_leaderboard.feed.all')}
209-
</Text>
236+
<Box style={styles.labelWrap}>
237+
<Text
238+
variant={TextVariant.BodyMd}
239+
fontWeight={FontWeight.Regular}
240+
color={TextColor.TextAlternative}
241+
>
242+
{strings('social_leaderboard.feed.all')}
243+
</Text>
244+
<Animated.View
245+
style={[styles.labelActive, allActiveStyle]}
246+
pointerEvents="none"
247+
>
248+
<Text
249+
variant={TextVariant.BodyMd}
250+
fontWeight={FontWeight.Medium}
251+
color={TextColor.TextDefault}
252+
>
253+
{strings('social_leaderboard.feed.all')}
254+
</Text>
255+
</Animated.View>
256+
</Box>
210257
</Box>
211258
</TouchableOpacity>
212259
</Box>

app/components/Views/SocialLeaderboard/FeedView/components/FeedTypeSheet.tsx

Lines changed: 75 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ import {
1414
TextVariant,
1515
} from '@metamask/design-system-react-native';
1616
import React, { useCallback, useMemo } from 'react';
17-
import { Pressable, StyleSheet } from 'react-native';
17+
import { Modal, Pressable, StyleSheet } from 'react-native';
18+
import { GestureHandlerRootView } from 'react-native-gesture-handler';
1819
import { strings } from '../../../../../../locales/i18n';
1920
import { playSelection } from '../../../../../util/haptics';
2021
import { useTheme } from '../../../../../util/theme';
@@ -25,6 +26,12 @@ import {
2526
} from '../FeedView.testIds';
2627
import { FEED_TYPE_LABEL_KEY, FEED_TYPE_OPTIONS } from './feedTypeOptions';
2728

29+
const styles = StyleSheet.create({
30+
root: {
31+
flex: 1,
32+
},
33+
});
34+
2835
export interface FeedTypeSheetProps {
2936
isOpen: boolean;
3037
value: FeedTypeFilter;
@@ -66,61 +73,76 @@ const FeedTypeSheetInner: React.FC<FeedTypeSheetInnerProps> = ({
6673
);
6774

6875
return (
69-
<Box twClassName="absolute inset-0">
70-
<Pressable
71-
style={[
72-
StyleSheet.absoluteFill,
73-
{ backgroundColor: colors.overlay.default },
74-
]}
75-
onPress={onClose}
76-
accessibilityRole="button"
77-
testID={FeedViewSelectorsIDs.TYPE_SELECTOR_BACKDROP}
78-
/>
76+
// Render at the root via a full-screen Modal so the backdrop covers the
77+
// entire surface (header + tabs included) instead of being clipped to the
78+
// pager page. React context (theme, safe-area, redux) is preserved because
79+
// the children stay in the same React tree; GestureHandlerRootView keeps the
80+
// sheet's swipe-to-dismiss working inside the Modal's native view hierarchy.
81+
<Modal
82+
visible
83+
transparent
84+
animationType="none"
85+
statusBarTranslucent
86+
onRequestClose={onClose}
87+
>
88+
<GestureHandlerRootView style={styles.root}>
89+
<Box twClassName="absolute inset-0">
90+
<Pressable
91+
style={[
92+
StyleSheet.absoluteFill,
93+
{ backgroundColor: colors.overlay.default },
94+
]}
95+
onPress={onClose}
96+
accessibilityRole="button"
97+
testID={FeedViewSelectorsIDs.TYPE_SELECTOR_BACKDROP}
98+
/>
7999

80-
<BottomSheetDialog
81-
onClose={onClose}
82-
testID={FeedViewSelectorsIDs.TYPE_SELECTOR_SHEET}
83-
>
84-
<Box twClassName="px-4 pt-2 pb-4">
85-
{options.map((option) => {
86-
const isSelected = option.key === value;
87-
return (
88-
<Pressable
89-
key={option.key}
90-
onPress={() => handleSelect(option.key)}
91-
accessibilityRole="button"
92-
accessibilityState={{ selected: isSelected }}
93-
testID={getFeedTypeOptionTestId(option.key)}
94-
>
95-
<Box
96-
flexDirection={BoxFlexDirection.Row}
97-
alignItems={BoxAlignItems.Center}
98-
justifyContent={BoxJustifyContent.Between}
99-
twClassName="py-3"
100-
>
101-
<Text
102-
variant={TextVariant.BodyMd}
103-
fontWeight={
104-
isSelected ? FontWeight.Medium : FontWeight.Regular
105-
}
106-
color={TextColor.TextDefault}
100+
<BottomSheetDialog
101+
onClose={onClose}
102+
testID={FeedViewSelectorsIDs.TYPE_SELECTOR_SHEET}
103+
>
104+
<Box twClassName="px-4 pt-2 pb-4">
105+
{options.map((option) => {
106+
const isSelected = option.key === value;
107+
return (
108+
<Pressable
109+
key={option.key}
110+
onPress={() => handleSelect(option.key)}
111+
accessibilityRole="button"
112+
accessibilityState={{ selected: isSelected }}
113+
testID={getFeedTypeOptionTestId(option.key)}
107114
>
108-
{option.label}
109-
</Text>
110-
{isSelected ? (
111-
<Icon
112-
name={IconName.Check}
113-
size={IconSize.Md}
114-
color={IconColor.PrimaryDefault}
115-
/>
116-
) : null}
117-
</Box>
118-
</Pressable>
119-
);
120-
})}
115+
<Box
116+
flexDirection={BoxFlexDirection.Row}
117+
alignItems={BoxAlignItems.Center}
118+
justifyContent={BoxJustifyContent.Between}
119+
twClassName="py-3"
120+
>
121+
<Text
122+
variant={TextVariant.BodyMd}
123+
fontWeight={
124+
isSelected ? FontWeight.Medium : FontWeight.Regular
125+
}
126+
color={TextColor.TextDefault}
127+
>
128+
{option.label}
129+
</Text>
130+
{isSelected ? (
131+
<Icon
132+
name={IconName.Check}
133+
size={IconSize.Md}
134+
color={IconColor.PrimaryDefault}
135+
/>
136+
) : null}
137+
</Box>
138+
</Pressable>
139+
);
140+
})}
141+
</Box>
142+
</BottomSheetDialog>
121143
</Box>
122-
</BottomSheetDialog>
123-
</Box>
144+
</GestureHandlerRootView>
145+
</Modal>
124146
);
125147
};
126148

app/components/Views/SocialLeaderboard/FeedView/hooks/useTraderFeed.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React from 'react';
22
import { act, renderHook, waitFor } from '@testing-library/react-native';
33
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
44
import { useTraderFeed } from './useTraderFeed';
5+
import type { FeedTypeFilter } from '../types';
56
import {
67
mockFeedResponse,
78
mockPerpFeedItem,
@@ -219,11 +220,11 @@ describe('useTraderFeed', () => {
219220
);
220221

221222
const { result, rerender } = renderHook(
222-
({ typeFilter }: { typeFilter: 'tokens' | 'perps' }) =>
223+
({ typeFilter }: { typeFilter: FeedTypeFilter }) =>
223224
useTraderFeed({ typeFilter }),
224225
{
225226
wrapper: createWrapper(),
226-
initialProps: { typeFilter: 'tokens' as const },
227+
initialProps: { typeFilter: 'tokens' },
227228
},
228229
);
229230

app/components/Views/SocialLeaderboard/TopTradersView/TopTradersView.test.tsx

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -73,13 +73,15 @@ const mockHasNotificationPreferences = jest.fn(() => true);
7373
const mockToggleTraderNotification = jest.fn();
7474
const mockIsTraderNotificationEnabled = jest.fn((_traderId: string) => true);
7575

76-
let mockNotificationPreferences = {
76+
const defaultNotificationPreferences = {
7777
...DEFAULT_SOCIAL_AI_PREFERENCES,
7878
mutedTraderProfileIds: [
7979
...DEFAULT_SOCIAL_AI_PREFERENCES.mutedTraderProfileIds,
8080
],
8181
};
8282

83+
let mockNotificationPreferences = { ...defaultNotificationPreferences };
84+
8385
const channelsDisabledPreferences = {
8486
...DEFAULT_SOCIAL_AI_PREFERENCES,
8587
pushNotificationsEnabled: false,
@@ -277,12 +279,7 @@ describe('TopTradersView', () => {
277279
mockSelectSocialLeaderboardEnabled.mockReturnValue(true);
278280
mockSelectSocialLeaderboardPerpsEnabled.mockReturnValue(true);
279281
mockHasNotificationPreferences.mockReturnValue(true);
280-
mockNotificationPreferences = {
281-
...DEFAULT_SOCIAL_AI_PREFERENCES,
282-
mutedTraderProfileIds: [
283-
...DEFAULT_SOCIAL_AI_PREFERENCES.mutedTraderProfileIds,
284-
],
285-
};
282+
mockNotificationPreferences = { ...defaultNotificationPreferences };
286283
mockIsTraderNotificationEnabled.mockReturnValue(true);
287284
});
288285

0 commit comments

Comments
 (0)