@@ -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' ;
1016import {
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
4763export 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 */
6178const 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 >
0 commit comments